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/.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 6d4fd7a49e..6aecb54402 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,7 @@ endif() option(ANYPS5_ENABLE_SPIRV_TOOLS "Enable SPIRV-Tools validation and optimization" OFF) option(ANYPS5_ENABLE_ASAN "Build Linux targets with AddressSanitizer and use an ASan-compatible guest arena range" OFF) +option(ANYPS5_ENABLE_TSAN "Build Linux targets with ThreadSanitizer" OFF) option(APS5_ENABLE_TIMING_LOG "Write frame timings asynchronously to frame-timing.log" OFF) option(APS5_ENABLE_TRACY "Build the Tracy profiler client and report performance timer scopes as Tracy zones" OFF) option(ANYPS5_USE_SYSTEM_DEPS "Resolve SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools from the system instead of the bundled submodules" OFF) @@ -76,6 +77,18 @@ if(NOT ANYPS5_CXX_LINKS_STANDARD_LIBRARY) "Supported toolchains are listed in docs/dev/BUILD.md.") endif() +if(ANYPS5_ENABLE_ASAN AND ANYPS5_ENABLE_TSAN) + message(FATAL_ERROR "ANYPS5_ENABLE_ASAN and ANYPS5_ENABLE_TSAN cannot be combined") +endif() +if(ANYPS5_ENABLE_TSAN) + if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") + message(FATAL_ERROR "ANYPS5_ENABLE_TSAN is supported only on Linux") + endif() + if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang)$") + message(FATAL_ERROR "ANYPS5_ENABLE_TSAN requires GCC or Clang") + endif() +endif() + if(ANYPS5_ENABLE_ASAN) if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") message(FATAL_ERROR "ANYPS5_ENABLE_ASAN is supported only on Linux") @@ -86,6 +99,10 @@ if(ANYPS5_ENABLE_ASAN) add_compile_options(-fsanitize=address -fno-omit-frame-pointer) add_link_options(-fsanitize=address) add_compile_definitions(ANYPS5_ENABLE_ASAN=1) +elseif(ANYPS5_ENABLE_TSAN) + add_compile_options(-fsanitize=thread -fno-omit-frame-pointer) + add_link_options(-fsanitize=thread) + add_compile_definitions(ANYPS5_ENABLE_TSAN=1) elseif(NOT APPLE) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -static-libgcc -static-libstdc++") endif() @@ -169,8 +186,15 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_read_encoding.py) add_test(NAME import_audit COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py) + if(NOT APPLE) + add_test(NAME annotate_module COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_annotate_module.py) + set_tests_properties(annotate_module PROPERTIES TIMEOUT 20) + endif() 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) @@ -213,6 +237,12 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedRoundTripTests) add_test(NAME configure_seed_parsing COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ChecksParsingTests) + add_test(NAME vulkan_validation_gate_reports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_vulkan_validation_gate.py ReportTests) + add_test(NAME vulkan_validation_gate_integration COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_vulkan_validation_gate.py IntegrationTests) + add_test(NAME vulkan_validation_gate_attribution COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_vulkan_validation_gate.py AttributionTests) add_test(NAME stubgen COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen.py) add_test(NAME debt_split COMMAND ${Python3_EXECUTABLE} @@ -255,7 +285,7 @@ if(BUILD_TESTING) 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) + set_tests_properties(check_compile_modes_${test_case} PROPERTIES TIMEOUT 60) endforeach() add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py) @@ -290,7 +320,9 @@ if(BUILD_TESTING) ${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) @@ -371,6 +403,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/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 3273aa1c3c..354299bd3d 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -63,6 +63,13 @@ function(configure_windows_unwind target) endif() endfunction() +function(configure_macos_unwind target) + if(APPLE) + target_compile_options(${target} PRIVATE -femit-dwarf-unwind=always) + target_link_options(${target} PRIVATE LINKER:-no_compact_unwind) + endif() +endfunction() + if(MINGW) foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") @@ -105,6 +112,7 @@ if(WIN32) target_compile_options(libc PRIVATE "SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/include/HostStandardStreams.hpp") endif() configure_windows_unwind(libc) +configure_macos_unwind(libc) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(libc PRIVATE LINKER:-z,now) target_link_options(libc INTERFACE LINKER:--no-as-needed) @@ -269,6 +277,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 +295,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) @@ -301,7 +344,7 @@ if(BUILD_TESTING) set(public_header_manifest "${CMAKE_CURRENT_BINARY_DIR}/public_headers.json") add_test(NAME public_headers COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_public_headers.py --audit-only --manifest "${public_header_manifest}") - set_tests_properties(public_headers PROPERTIES TIMEOUT 30 RUN_SERIAL TRUE FIXTURES_SETUP public_headers_manifest) + set_tests_properties(public_headers PROPERTIES TIMEOUT 90 RUN_SERIAL TRUE FIXTURES_SETUP public_headers_manifest) foreach(batch RANGE 0 63) add_test(NAME public_headers_compile_${batch} COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_public_headers.py --compiler ${CMAKE_CXX_COMPILER} ${public_header_compiler_args} @@ -329,6 +372,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) @@ -426,6 +474,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) @@ -475,6 +535,17 @@ if(BUILD_TESTING) PASS_REGULAR_EXPRESSION "C\\+\\+ exception St9bad_alloc raised on thread [0-9]+ cannot be delivered: 0x[0-9a-f]+ undeliverable_host_exception_tests\\.exe\\+0x[0-9a-f]+ has no unwind data") endif() + add_test_executable(guest_kernel_sys_uuid_tests tests/GuestKernelSysUuid.cpp) + target_include_directories(guest_kernel_sys_uuid_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_uuid_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_uuid_tests) + add_test(NAME guest_kernel_sys_uuid COMMAND guest_kernel_sys_uuid_tests) + set_tests_properties(guest_kernel_sys_uuid PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_kernel_sys_uuid PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(crash_text_tests tests/CrashText.cpp) target_include_directories(crash_text_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) if(MINGW) 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..fbc538e972 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(libSceAgc SHARED EXCLUDE_FROM_ALL + ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Acb/src/Control.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Acb/src/Dispatch.cpp @@ -293,6 +294,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/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/Platform.cpp b/core/libs/prx/libSceAgc/tests/Platform.cpp index 5ed2b2aea1..dd7104fd07 100644 --- a/core/libs/prx/libSceAgc/tests/Platform.cpp +++ b/core/libs/prx/libSceAgc/tests/Platform.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgc/Misc/include/Platform.hpp" +#include "prx/libSceAgc/include/Agc.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/Shutdown.hpp" @@ -34,12 +35,18 @@ void testRejections() { expectFailure([] { sceAgcGetIsTrinityMode(nullptr); }); } +void testExportLinked() { + auto* volatile linked = &sceAgcSuspendPointAndCheckStatus; + check(linked != nullptr, "sceAgcSuspendPointAndCheckStatus is not linked"); +} + } int main() { try { testTrinityMode(); testRejections(); + testExportLinked(); LibcRunShutdown_nid_postfix(); std::puts("AGC platform tests passed"); return 0; 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 0b65806855..641225581a 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -238,6 +238,7 @@ foreach(agcOverlayStage IN ITEMS Vert Frag) endforeach() set(agcDriverSources + ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ComputePipelineCapture.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/DisplayBuffer.cpp @@ -327,6 +328,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 @@ -1279,6 +1281,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) @@ -1572,8 +1588,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) @@ -1832,10 +1852,14 @@ if(BUILD_TESTING) set_tests_properties(agc_recorder_cmask PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_recorder_cube COMMAND agc_driver_recorder_tests --cube-only) set_tests_properties(agc_recorder_cube PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_recorder_failed_check COMMAND agc_driver_recorder_tests --failed-check-only) + set_tests_properties(agc_recorder_failed_check PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 + PASS_REGULAR_EXPRESSION "a failed check stopped the test with a texture upload in its open batch" + FAIL_REGULAR_EXPRESSION "recorder teardown") if(WIN32) set_tests_properties(agc_driver_host_import_lifetime agc_driver_host_import_mutation_race agc_driver_unit_shadow agc_driver_unit_shadow_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") - set_tests_properties(agc_recorder_completion_labels agc_recorder_cmask agc_recorder_cube PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_recorder_completion_labels agc_recorder_cmask agc_recorder_cube agc_recorder_failed_check PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() set_property(TEST agc_driver_host_import_lifetime APPEND PROPERTY ENVIRONMENT_MODIFICATION @@ -2172,6 +2196,18 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources) target_link_libraries(${agcExecutionTest} PRIVATE libSceAgcDriver SDL2-static libc) set_target_properties(${agcExecutionTest} PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(${agcExecutionTest}) + if(agcExecutionTests) + list(GET agcExecutionTests 0 agcExecutionPchTarget) + target_precompile_headers(${agcExecutionTest} REUSE_FROM ${agcExecutionPchTarget}) + else() + target_precompile_headers(${agcExecutionTest} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/VulkanTestDevice.hpp + ) + endif() + if(CMAKE_CXX_COMPILER_LAUNCHER MATCHES "ccache") + set_target_properties(${agcExecutionTest} PROPERTIES CXX_COMPILER_LAUNCHER + "${CMAKE_COMMAND};-E;env;CCACHE_SLOPPINESS=pch_defines,time_macros;${CMAKE_CXX_COMPILER_LAUNCHER}") + endif() list(APPEND agcExecutionTests ${agcExecutionTest}) endforeach() if(BUILD_TESTING) @@ -2317,6 +2353,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}") @@ -2367,6 +2406,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) @@ -2386,6 +2426,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) @@ -2524,6 +2566,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}") @@ -2531,7 +2579,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") @@ -2860,12 +2908,12 @@ if(BUILD_TESTING) ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_f64_denorm_preserve_spirv COMMAND agc_driver_f64_fma_subnormals_tests --verify-spv-denorm-mode) - set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_f16_to_f32_unpack_spirv COMMAND agc_driver_f16_to_f32_unpack_tests --verify-spirv) - set_tests_properties(agc_driver_f16_to_f32_unpack_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_driver_f16_to_f32_unpack_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) set_tests_properties(agc_driver_f16_to_f32_unpack_spirv PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() @@ -2897,12 +2945,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 @@ -3465,8 +3517,7 @@ if(BUILD_TESTING) endif() endforeach() - option(ANYPS5_RUN_QUARANTINED_FRAME_REPLAY "Build and register agc_driver_frame_replay on Windows" OFF) - if(WIN32 AND ANYPS5_RUN_QUARANTINED_FRAME_REPLAY) + if(WIN32) add_test_executable(agc_driver_frame_replay_tests tests/FrameReplay.cpp Execution/src/FrameCapture.cpp) target_include_directories(agc_driver_frame_replay_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_driver_frame_replay_tests PRIVATE libSceAgc libSceAgcDriver libkernel libc libzstd_static) @@ -3479,6 +3530,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() 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 86505f5a7b..97a4339de3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp @@ -2,7 +2,9 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DIAGNOSTICS_HPP #include "prx/libSceAgcDriver/Execution/include/QueueState.hpp" +#include #include +#include #include #include @@ -21,6 +23,7 @@ public: bool SkipRefusedDraws(); void ReportRefusedDraw(const RefusedDraw& refused); +std::optional RefusedShaderCount(); void require(bool condition, const char* reason); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 18303f9f7b..7d89a2589f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -45,6 +45,7 @@ namespace AgcDriver { class PerformanceTimer; +struct OverlayDriverCounters; } namespace AgcDriver::DriverDetail { @@ -59,6 +60,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 +77,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); @@ -97,6 +100,7 @@ public: static void tagVariantRuns(DispatchVariant& variant, std::span regions, const ShaderRecompiler::SrtUserDataUse& use, bool eligible, std::uint32_t hiddenWords, std::span> supplied = {}); static std::string dumpRequest(std::uint64_t address, const ShaderRecompiler::RecompileRequest& request, std::string_view suffix = {}); DrawEntryCounters DrawCacheCounters(); + OverlayDriverCounters OverlayCounters(); EntryCounters DispatchCacheCounters(); struct DispatchCacheFootprint { std::size_t entries = 0; @@ -317,7 +321,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 +478,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/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index b1907257c4..0c19b88668 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -227,6 +227,7 @@ void SubmitLibraryPrebuild(std::function task); void AwaitLibraryPrebuilds(); void CloseRegistrationPreparation(); unsigned RegistrationPreparationThreads(); +std::optional RegistrationPreparationsPending(); void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target, bool deferUnprepared = false); 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/PerformanceOverlay.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp index 653ff19e76..8eca6e6190 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp @@ -4,6 +4,7 @@ #define VK_NO_PROTOTYPES #include #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" #include #include #include @@ -13,7 +14,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -26,12 +29,15 @@ class Buffer; enum class OverlayCorner : std::uint8_t { TopLeft, TopRight, BottomLeft, BottomRight }; +inline constexpr std::uint32_t OverlayPageCount = 3; + struct OverlaySettings { - bool visible = false; + std::uint32_t page = 0; OverlayCorner corner = OverlayCorner::TopLeft; }; OverlaySettings ParseOverlaySettings(const char* enabled, const char* position); +std::uint32_t OverlayPageAfter(std::uint32_t start, std::uint64_t presses); class PerformanceCounters { public: @@ -123,10 +129,26 @@ struct OverlayRect { enum class OverlayColor : std::uint32_t { Background = 0, Text = 1, Graph = 2 }; inline constexpr std::size_t OverlayColorCount = 3; +struct OverlayMetrics { + static constexpr std::int32_t GlyphWidth = 5; + static constexpr std::int32_t GlyphHeight = 7; + static constexpr std::int32_t Advance = 6; + static constexpr std::int32_t LinePitch = 9; + static constexpr std::int32_t Columns = 27; + static constexpr std::int32_t Padding = 3; + static constexpr std::int32_t Margin = 4; + static constexpr std::int32_t GraphHeight = 20; + static constexpr std::int32_t GraphGap = 2; + static constexpr std::int32_t InnerWidth = Columns * Advance - 1; + static constexpr std::int32_t DetailColumns = 46; +}; + struct OverlayContent { std::vector above; std::vector frameTimes; std::vector below; + std::int32_t columns = OverlayMetrics::Columns; + bool graph = true; }; struct OverlayMemory { @@ -141,35 +163,100 @@ struct OverlayDetails { VkExtent2D window{}; }; +struct OverlayThreadBusy { + std::string name; + double percent = 0; +}; + +struct OverlayPhase { + std::string name; + double msPerFrame = 0; +}; + +struct OverlayQueueWaits { + double memoryMs = 0; + double lockMs = 0; + double gpuMs = 0; +}; + +struct OverlayDetailSample { + double fps = 0; + double frameMs = 0; + std::vector busy; + std::vector phases; + std::optional queueWaits; + std::optional renderPassesPerFrame; + std::optional pipelinesCompiling; + std::optional pipelinesPerSecond; + std::optional drawCacheHitPercent; + std::optional pipelineCacheHitPercent; + std::optional flipsPerSecond; + std::optional presentsPerSecond; + std::optional droppedFrames; + std::optional repeatedPresents; + std::optional commit; + std::optional pageFaultsPerSecond; + std::optional hostImports; + std::optional hostImportBytes; + std::optional cpuCopyMegabytesPerSecond; + std::optional addressCopyCapPercent; + std::optional unimplementedPerSecond; + std::optional unimplementedTotal; + std::optional refusedShaders; + std::string title; + std::string presentMode; +}; + +struct OverlayHits { + std::uint64_t lookups = 0; + std::uint64_t hits = 0; +}; + +struct OverlayDriverCounters { + std::uint64_t flips = 0; + std::optional drawCache; +}; + +struct OverlayPresentInfo { + std::uint64_t displayAddress = 0; + VkPresentModeKHR presentMode = VK_PRESENT_MODE_MAX_ENUM_KHR; + std::uint64_t acquireDrops = 0; +}; + +struct OverlayTraceFigures { + std::vector busy; + std::vector phases; + std::optional queueWaits; + std::optional renderPassesPerFrame; +}; + +inline constexpr std::size_t OverlayTopPhases = 5; + struct OverlayLayout { OverlayRect bounds; std::vector rects; std::int32_t scale = 1; }; -struct OverlayMetrics { - static constexpr std::int32_t GlyphWidth = 5; - static constexpr std::int32_t GlyphHeight = 7; - static constexpr std::int32_t Advance = 6; - static constexpr std::int32_t LinePitch = 9; - static constexpr std::int32_t Columns = 27; - static constexpr std::int32_t Padding = 3; - static constexpr std::int32_t Margin = 4; - static constexpr std::int32_t GraphHeight = 20; - static constexpr std::int32_t GraphGap = 2; - static constexpr std::int32_t InnerWidth = Columns * Advance - 1; -}; - const std::array& OverlayGlyph(char character); +std::string_view OverlayFontCharacters(); std::string FormatOverlayValue(double value); std::string FormatOverlayRate(double value); std::string FormatOverlayCount(double value); std::string FormatOverlayGigabytes(std::uint64_t bytes); OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver); OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver, const OverlayDetails& details); +std::string OverlayText(std::string_view text); +OverlayContent ComposeOverlayDetails(const OverlayDetailSample& sample); +OverlayTraceFigures OverlayTraceDeltas(std::span before, std::span now, OverlayStatistics::Clock::duration elapsed, std::uint64_t presents, bool passes); +std::optional OverlayHitPercent(const OverlayHits& before, const OverlayHits& now); +std::string_view OverlayPresentModeName(VkPresentModeKHR mode); bool OverlaySampleDue(std::optional sampledAt, OverlayStatistics::Clock::time_point now); std::optional ReadVramBudget(const Graphics::Context& context); std::optional ReadProcessPrivateBytes(); +std::optional ParseCommitCharge(std::string_view meminfo); +std::optional ReadCommitCharge(); +std::optional ReadPageFaults(); std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content); OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner); std::array, OverlayColorCount> OverlayPalette(VkFormat format, VkColorSpaceKHR colorSpace); @@ -182,12 +269,16 @@ public: PerformanceOverlay& operator=(const PerformanceOverlay&) = delete; static bool Visible(); + static std::uint32_t Page(); static void Toggle(); static OverlayCorner Corner(); + static void SetDriverCounters(OverlayDriverCounters (*source)()); - void NotePresent(OverlayStatistics::Clock::time_point at, VkExtent2D display); + void NotePresent(OverlayStatistics::Clock::time_point at, VkExtent2D display, const OverlayPresentInfo& info = {}); const OverlayStatistics& Statistics() const { return statistics; } const OverlayDetails& Details() const { return details; } + const OverlayDetailSample& DetailSample() const { return detailSample; } + std::uint32_t ShownPage() const { return page; } OverlayRect Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout); OverlayRect Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout, const OverlayContent& content, OverlayCorner corner); void Release(std::size_t slot) noexcept; @@ -203,7 +294,29 @@ private: VkPipeline pipeline(VkFormat format, VkRenderPass compatible); void releaseTransient(Slot& slot) noexcept; void release() noexcept; + struct DetailCounters { + std::optional driver; + OverlayHits pipelines; + std::uint64_t copiedBytes = 0; + std::optional pageFaults; + std::uint64_t unimplemented = 0; + std::uint64_t acquireDrops = 0; + }; + + struct DetailState { + std::optional sampledAt; + DetailCounters base; + std::uint64_t repeats = 0; + std::uint64_t lastDisplayAddress = 0; + std::optional traceSampledAt; + std::vector traceBase; + std::uint64_t tracePresents = 0; + }; + void restartIfToggled(); + void noteDetailPresent(OverlayStatistics::Clock::time_point at, const OverlayPresentInfo& info); + void sampleDetails(OverlayStatistics::Clock::time_point at, const OverlayPresentInfo& info); + void sampleTrace(OverlayStatistics::Clock::time_point at); Graphics::Context context; VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; @@ -214,8 +327,11 @@ private: std::vector slots; OverlayStatistics statistics; OverlayDetails details; + OverlayDetailSample detailSample; + DetailState detail; std::optional sampledAt; std::uint64_t toggles = 0; + std::uint32_t page = 0; }; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp index 49858aaa31..71c65e2bce 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp @@ -32,6 +32,7 @@ class TraceThread; extern "C" bool AgcDriverTraceRecording_nid_postfix(); extern "C" AgcDriver::TraceThread* AgcDriverTraceThread_nid_postfix(); +extern "C" void AgcDriverTraceCount_nid_postfix(const char* scope); namespace AgcDriver { @@ -769,8 +770,15 @@ private: } }; +struct TraceThreadTotals { + std::string name; + std::vector totals; +}; + class PerformanceTrace { public: + static constexpr const char* RenderPassScope = "Graphics.RenderPass"; + static bool Configured() { static const bool configured = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr || std::getenv("APS5_TRACE_SUMMARY_EVERY") != nullptr || std::getenv("APS5_TRACE_BENCH") != nullptr; return configured; @@ -797,11 +805,21 @@ public: return AgcDriverTraceThread_nid_postfix(); } + static void Count(const char* scope) { + if (Recording()) AgcDriverTraceCount_nid_postfix(scope); + } + + static void SetOverlayTotals(bool on); + static bool OverlayTotals(); + static std::optional> ThreadTotals(); static void StopOutput(); private: friend bool ::AgcDriverTraceRecording_nid_postfix(); friend TraceThread* ::AgcDriverTraceThread_nid_postfix(); + friend void ::AgcDriverTraceCount_nid_postfix(const char* scope); + + static TraceThread* bindNamed(); static void OpenInterval(std::chrono::steady_clock::time_point now, std::uint64_t present); static void FlushInterval(std::chrono::steady_clock::time_point now, std::uint32_t width, std::uint32_t height, const VulkanDevice* device); @@ -810,6 +828,8 @@ private: inline static std::atomic buffering{false}; inline static std::atomic presentCount{0}; static thread_local TraceThread* local; + static thread_local const char* localName; + static std::atomic binder; inline static std::mutex gpuMutex; inline static std::vector gpuRanges; inline static std::uint64_t gpuLost = 0; 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/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 1c7d1a1350..1e380f74b4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -259,9 +259,9 @@ 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; @@ -275,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 { @@ -392,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 ca92adc259..85f7d2f0ca 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp @@ -88,18 +88,38 @@ bool SkipRefusedDraws() { return skip; } +namespace { + +struct RefusedShaders { + std::mutex mutex; + std::set reported; +}; + +RefusedShaders& refusedShaders() { + static RefusedShaders shaders; + return shaders; +} + +} + void ReportRefusedDraw(const RefusedDraw& refused) { - static std::mutex reportedMutex; - static std::set reported; + auto& shaders = refusedShaders(); { - std::lock_guard lock(reportedMutex); - if (!reported.insert(refused.address).second) return; + std::lock_guard lock(shaders.mutex); + if (!shaders.reported.insert(refused.address).second) return; } const std::string_view what = refused.what(); const auto line = what.substr(0, what.find('\n')); std::fprintf(stderr, "[gpu] APS5_SKIP_REFUSED_DRAWS: skipping the draws that use shader 0x%llx: %.*s\n", static_cast(refused.address), static_cast(line.size()), line.data()); } +std::optional RefusedShaderCount() { + if (!SkipRefusedDraws()) return std::nullopt; + auto& shaders = refusedShaders(); + std::lock_guard lock(shaders.mutex); + return shaders.reported.size(); +} + void require(bool condition, const char* reason) { if (!condition) { throw std::runtime_error(std::string("AGC driver: ") + reason); 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 528da65d87..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() { @@ -281,6 +295,14 @@ void Driver::dispatch(QueueState& queue, std::span packet, fromAhead = false; ++groupStats.keyMismatches; } + if (fromAhead) { + if (const auto hook = aheadAdoptHook.load(std::memory_order_acquire)) hook(); + if (!CapturedCurrent(ahead.captured, snapshot)) { + fromAhead = false; + ++dispatchesRejectedAhead; + ++groupStats.memoryMismatches; + } + } phaseTiming.Phase(PhaseKey); timing.Mark("decode_key_device_setup"); bool readsClear = false; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp index 0ebd368419..50a5eee57b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp @@ -42,6 +42,14 @@ std::uint64_t Driver::DispatchesAdoptedAhead() const { return dispatchesAdoptedAhead.load(std::memory_order_relaxed); } +std::uint64_t Driver::DispatchesRejectedAhead() const { + return dispatchesRejectedAhead.load(std::memory_order_relaxed); +} + +void Driver::SetAheadAdoptHook(void (*hook)()) { + aheadAdoptHook.store(hook, std::memory_order_release); +} + namespace { std::size_t& PreviewedUntil() { @@ -326,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/DrawCacheStatistics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp index b1f3dc2a02..dc0211bb25 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include namespace AgcDriver::DriverDetail { @@ -46,6 +47,13 @@ DrawEntryCounters Driver::DrawCacheCounters() { return drawEntryCounters; } +OverlayDriverCounters Driver::OverlayCounters() { + OverlayDriverCounters counters{flipsCounted.load(std::memory_order_relaxed), std::nullopt}; + std::unique_lock cacheLock(drawCacheMutex, std::try_to_lock); + if (cacheLock.owns_lock()) counters.drawCache = OverlayHits{drawEntryCounters.lookups, drawEntryCounters.hits}; + return counters; +} + std::string Driver::profileDrawCacheLines(const DrawEntryCounters& counters) { std::string text; const auto append = [&](const char* format, auto... values) { 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/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/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index ba19bb4a60..e12649fac4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" @@ -230,6 +231,7 @@ Driver::Driver() { gpuMapImportThreads.fetch_add(1, std::memory_order_relaxed); host.join(); }); + PerformanceOverlay::SetDriverCounters([] { return Driver::Get().OverlayCounters(); }); } void Driver::WaitIdle() { 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 5972698258..7e0d9e571c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -71,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); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp index ce28441eb1..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"); @@ -130,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) { @@ -206,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/Queues/QueueWorker.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp index 546be83644..b6a5b5689c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp @@ -146,7 +146,7 @@ void Driver::run(std::uint32_t id) noexcept { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; auto& costs = submissionCosts(id); try { - if (PerformanceTrace::Configured()) PerformanceTrace::BindThread(role); + PerformanceTrace::BindThread(role); for (;;) { submission = Submission{}; struct Running { 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..7c8efbdf04 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(); } @@ -1498,6 +1498,17 @@ public: return liveWorkers.load(std::memory_order_acquire); } + static std::optional Pending() { + std::size_t pending = 0; + for (auto* pool : {Existing(), ExistingPrebuild()}) { + if (pool == nullptr) continue; + std::unique_lock lock(pool->mutex, std::try_to_lock); + if (!lock.owns_lock()) return std::nullopt; + pending += pool->tasks.size() + pool->running; + } + return pending; + } + static void CloseAll() { std::array pools{}; { @@ -1637,7 +1648,7 @@ private: static void* Main(void* argument) { auto& pool = *static_cast(argument); - if (PerformanceTrace::Configured()) PerformanceTrace::BindThread("shader prepare", TraceThread::DefaultCapacity / 8); + PerformanceTrace::BindThread("shader prepare", TraceThread::DefaultCapacity / 8); for (;;) { Task task; { @@ -1750,6 +1761,10 @@ unsigned RegistrationPreparationThreads() { return RegistrationPreparePool::LiveWorkers(); } +std::optional RegistrationPreparationsPending() { + return RegistrationPreparePool::Pending(); +} + std::vector PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target, bool deferUnprepared) { ShaderPreparationTransaction transaction; require(decoded.programs.size() == decoded.roles.size(), "graphics ABI program roles are incomplete"); 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 b7290d5db1..e72fabda25 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1825,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/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp index ca74a794d9..e0c805d9d4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -1,19 +1,27 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" #include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlayFrag_spv.h" #include "prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlayVert_spv.h" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libc/include/General.hpp" #include "prx/libc/include/HostThreadLocal.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include #include #include #include #include #include +#include #include +#include #include #include #include @@ -24,6 +32,8 @@ #endif #include #include +#else +#include #endif namespace AgcDriver { @@ -37,9 +47,11 @@ struct OverlayParameters { }; std::atomic toggleCount{0}; +std::atomic currentPage{0}; std::atomic gpuTotal{0}; std::atomic driverTotal{0}; std::atomic pipelineTotal{0}; +std::atomic driverCounters{nullptr}; struct PacketCounterRegistry { std::mutex mutex; @@ -65,23 +77,45 @@ struct PacketCounterLease { constexpr double GigabyteBytes = 1024.0 * 1024.0 * 1024.0; const OverlaySettings& settings() { - static const OverlaySettings parsed = ParseOverlaySettings(std::getenv("ANYPS5_OVERLAY"), std::getenv("ANYPS5_OVERLAY_POS")); + static const OverlaySettings parsed = [] { + const auto read = ParseOverlaySettings(std::getenv("ANYPS5_OVERLAY"), std::getenv("ANYPS5_OVERLAY_POS")); + currentPage.store(read.page, std::memory_order_relaxed); + if (read.page == 2) PerformanceTrace::SetOverlayTotals(true); + return read; + }(); return parsed; } -std::int32_t panelWidth(std::int32_t scale) { - return (OverlayMetrics::InnerWidth + 2 * OverlayMetrics::Padding) * scale; +std::string busyName(std::string_view thread) { + auto queue = thread.substr(TraceFormat::WorkerPrefix.size()); + if (queue.starts_with("0x")) queue.remove_prefix(2); + return "Q" + std::string(queue); +} + +std::optional meminfoKilobytes(std::string_view line, std::string_view name) { + if (!line.starts_with(name)) return std::nullopt; + auto rest = line.substr(name.size()); + while (!rest.empty() && rest.front() == ' ') rest.remove_prefix(1); + std::uint64_t value = 0; + const auto [last, error] = std::from_chars(rest.data(), rest.data() + rest.size(), value); + if (error != std::errc{} || std::string_view(last, static_cast(rest.data() + rest.size() - last)) != " kB") return std::nullopt; + return value * 1024u; +} + +std::int32_t panelWidth(std::int32_t columns, std::int32_t scale) { + return (columns * OverlayMetrics::Advance - 1 + 2 * OverlayMetrics::Padding) * scale; } std::int32_t panelHeight(const OverlayContent& content, std::int32_t scale) { const auto lines = static_cast(content.above.size() + content.below.size()); const auto glyphGap = OverlayMetrics::LinePitch - OverlayMetrics::GlyphHeight; - return (2 * OverlayMetrics::Padding + lines * OverlayMetrics::LinePitch + OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap - glyphGap) * scale; + const auto graph = content.graph ? OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap : 0; + return (2 * OverlayMetrics::Padding + lines * OverlayMetrics::LinePitch + graph - glyphGap) * scale; } bool fits(VkExtent2D extent, const OverlayContent& content, std::int32_t scale) { const auto margins = 2 * OverlayMetrics::Margin * scale; - return panelWidth(scale) + margins <= static_cast(extent.width) && panelHeight(content, scale) + margins <= static_cast(extent.height); + return panelWidth(content.columns, scale) + margins <= static_cast(extent.width) && panelHeight(content, scale) + margins <= static_cast(extent.height); } float srgbDecode(float value) { @@ -106,9 +140,10 @@ bool srgbFormat(VkFormat format) { OverlaySettings ParseOverlaySettings(const char* enabled, const char* position) { OverlaySettings parsed; - const std::string_view visible = enabled != nullptr ? enabled : ""; - if (visible == "1") parsed.visible = true; - else if (!visible.empty() && visible != "0") throw std::runtime_error("ANYPS5_OVERLAY must be 1 or 0, not '" + std::string(visible) + "'"); + const std::string_view page = enabled != nullptr ? enabled : ""; + if (page == "1") parsed.page = 1; + else if (page == "2") parsed.page = 2; + else if (!page.empty() && page != "0") throw std::runtime_error("ANYPS5_OVERLAY must be 0, 1 or 2, not '" + std::string(page) + "'"); const std::string_view corner = position != nullptr ? position : ""; if (corner.empty() || corner == "tl") parsed.corner = OverlayCorner::TopLeft; else if (corner == "tr") parsed.corner = OverlayCorner::TopRight; @@ -118,6 +153,11 @@ OverlaySettings ParseOverlaySettings(const char* enabled, const char* position) return parsed; } +std::uint32_t OverlayPageAfter(std::uint32_t start, std::uint64_t presses) { + Graphics::Require(start < OverlayPageCount, "the performance overlay has no such page"); + return static_cast((start + presses % OverlayPageCount) % OverlayPageCount); +} + void PerformanceCounters::AddGpuNanoseconds(std::uint64_t nanoseconds) { gpuTotal.fetch_add(nanoseconds, std::memory_order_relaxed); } @@ -289,6 +329,10 @@ const std::array& OverlayGlyph(char c static constexpr Glyph period{0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x0c}; static constexpr Glyph colon{0x00, 0x0c, 0x0c, 0x00, 0x0c, 0x0c, 0x00}; static constexpr Glyph slash{0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x00}; + static constexpr Glyph percent{0x18, 0x19, 0x02, 0x04, 0x08, 0x13, 0x03}; + static constexpr Glyph plus{0x00, 0x04, 0x04, 0x1f, 0x04, 0x04, 0x00}; + static constexpr Glyph minus{0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00}; + static constexpr Glyph underscore{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f}; static constexpr Glyph a{0x0e, 0x11, 0x11, 0x11, 0x1f, 0x11, 0x11}; static constexpr Glyph b{0x1e, 0x11, 0x11, 0x1e, 0x11, 0x11, 0x1e}; static constexpr Glyph c{0x0e, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0e}; @@ -298,12 +342,14 @@ const std::array& OverlayGlyph(char c static constexpr Glyph g{0x0e, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0f}; static constexpr Glyph h{0x11, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11}; static constexpr Glyph i{0x0e, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e}; + static constexpr Glyph j{0x07, 0x02, 0x02, 0x02, 0x02, 0x12, 0x0c}; static constexpr Glyph k{0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11}; static constexpr Glyph l{0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1f}; static constexpr Glyph m{0x11, 0x1b, 0x15, 0x15, 0x11, 0x11, 0x11}; static constexpr Glyph n{0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11}; static constexpr Glyph o{0x0e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e}; static constexpr Glyph p{0x1e, 0x11, 0x11, 0x1e, 0x10, 0x10, 0x10}; + static constexpr Glyph q{0x0e, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0d}; static constexpr Glyph r{0x1e, 0x11, 0x11, 0x1e, 0x14, 0x12, 0x11}; static constexpr Glyph s{0x0f, 0x10, 0x10, 0x0e, 0x01, 0x01, 0x1e}; static constexpr Glyph t{0x1f, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04}; @@ -311,12 +357,18 @@ const std::array& OverlayGlyph(char c static constexpr Glyph v{0x11, 0x11, 0x11, 0x11, 0x11, 0x0a, 0x04}; static constexpr Glyph w{0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0x0a}; static constexpr Glyph x{0x11, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x11}; + static constexpr Glyph y{0x11, 0x11, 0x11, 0x0a, 0x04, 0x04, 0x04}; + static constexpr Glyph z{0x1f, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1f}; if (character >= '0' && character <= '9') return digits[static_cast(character - '0')]; switch (character) { case ' ': return space; case '.': return period; case ':': return colon; case '/': return slash; + case '%': return percent; + case '+': return plus; + case '-': return minus; + case '_': return underscore; case 'A': return a; case 'B': return b; case 'C': return c; @@ -326,12 +378,14 @@ const std::array& OverlayGlyph(char c case 'G': return g; case 'H': return h; case 'I': return i; + case 'J': return j; case 'K': return k; case 'L': return l; case 'M': return m; case 'N': return n; case 'O': return o; case 'P': return p; + case 'Q': return q; case 'R': return r; case 'S': return s; case 'T': return t; @@ -339,10 +393,16 @@ const std::array& OverlayGlyph(char c case 'V': return v; case 'W': return w; case 'X': return x; + case 'Y': return y; + case 'Z': return z; default: throw std::runtime_error(std::string("the performance overlay font has no glyph for '") + character + "'"); } } +std::string_view OverlayFontCharacters() { + return " .:/%+-_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +} + std::string FormatOverlayValue(double value) { const double clamped = std::isfinite(value) ? std::clamp(value, 0.0, 9999.0) : 0.0; char text[16]; @@ -393,7 +453,7 @@ OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, boo const auto size = [](VkExtent2D extent) { return std::to_string(extent.width) + "X" + std::to_string(extent.height); }; for (const std::string_view separator : {" / ", "/"}) { auto line = "RES: " + size(details.display) + std::string(separator) + size(details.window); - if (line.size() > static_cast(OverlayMetrics::Columns)) continue; + if (line.size() > static_cast(content.columns)) continue; content.below.push_back(std::move(line)); break; } @@ -401,6 +461,140 @@ OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, boo return content; } +std::string OverlayText(std::string_view text) { + const auto font = OverlayFontCharacters(); + std::string shown; + shown.reserve(text.size()); + for (const char character : text) { + const char upper = character >= 'a' && character <= 'z' ? static_cast(character - 'a' + 'A') : character; + shown.push_back(font.find(upper) != std::string_view::npos ? upper : ' '); + } + return shown; +} + +OverlayContent ComposeOverlayDetails(const OverlayDetailSample& sample) { + OverlayContent content; + content.columns = OverlayMetrics::DetailColumns; + content.graph = false; + const auto width = static_cast(content.columns); + const auto add = [&](std::string line) { + if (line.size() > width) line.resize(width); + content.below.push_back(std::move(line)); + }; + const auto joined = [&](const std::string& label, std::initializer_list> parts) { + std::string line = label; + bool any = false; + for (const auto& part : parts) { + if (!part) continue; + line += (line.empty() ? "" : any ? " " : " ") + *part; + any = true; + } + if (any) add(std::move(line)); + }; + const auto shown = [](const auto& value, auto format) -> std::optional { + if (!value) return std::nullopt; + return format(*value); + }; + const auto value = [](double number) { return FormatOverlayValue(number); }; + const auto count = [](double number) { return FormatOverlayCount(number); }; + const auto rate = [](double number) { return FormatOverlayRate(number); }; + add("FPS: " + rate(sample.fps) + " FRAME: " + value(sample.frameMs) + " MS"); + if (!sample.busy.empty()) { + const std::string label = "BUSY %:"; + std::string line = label; + for (const auto& thread : sample.busy) { + const auto item = " " + OverlayText(thread.name) + " " + count(thread.percent); + if (line.size() > label.size() && line.size() + item.size() > width) { + add(std::move(line)); + line = std::string(label.size(), ' '); + } + line += item; + } + add(std::move(line)); + } + for (const auto& phase : sample.phases) { + const auto amount = " " + value(phase.msPerFrame) + " MS/FRAME"; + auto name = OverlayText(phase.name); + name.resize(std::min(name.size(), width - amount.size())); + add(name + std::string(width - name.size() - amount.size(), ' ') + amount); + } + if (sample.queueWaits) add("Q0 WAIT MS/FRAME: MEM " + value(sample.queueWaits->memoryMs) + " LOCK " + value(sample.queueWaits->lockMs) + " GPU " + value(sample.queueWaits->gpuMs)); + joined("RENDER PASSES/FRAME:", {shown(sample.renderPassesPerFrame, value)}); + joined("PIPELINES:", {shown(sample.pipelinesCompiling, [&](std::uint64_t pending) { return "COMPILING " + count(static_cast(pending)); }), shown(sample.pipelinesPerSecond, [&](double made) { return "NEW/S " + count(made); })}); + joined("HIT %:", {shown(sample.drawCacheHitPercent, [&](double percent) { return "DRAW CACHE " + value(percent); }), shown(sample.pipelineCacheHitPercent, [&](double percent) { return "PIPELINES " + value(percent); })}); + joined("", {shown(sample.flipsPerSecond, [&](double flips) { return "FLIPS/S: " + rate(flips); }), shown(sample.presentsPerSecond, [&](double presents) { return "PRESENTS/S: " + rate(presents); })}); + joined("", {shown(sample.droppedFrames, [&](std::uint64_t dropped) { return "DROPPED/S: " + count(static_cast(dropped)); }), shown(sample.repeatedPresents, [&](std::uint64_t repeated) { return "REPEATED/S: " + count(static_cast(repeated)); })}); + joined("", {shown(sample.commit, [](const OverlayMemory& memory) { return "COMMIT: " + FormatOverlayGigabytes(memory.used) + "/" + FormatOverlayGigabytes(memory.budget) + " GB"; }), shown(sample.pageFaultsPerSecond, [&](double faults) { return "FAULTS/S: " + count(faults); })}); + joined("HOST IMPORTS:", {shown(sample.hostImports, [&](std::uint64_t imports) { return count(static_cast(imports)); }), shown(sample.hostImportBytes, [](const OverlayMemory& memory) { return FormatOverlayGigabytes(memory.used) + "/" + FormatOverlayGigabytes(memory.budget) + " GB"; })}); + joined("CPU COPIES:", {shown(sample.cpuCopyMegabytesPerSecond, [&](double megabytes) { return value(megabytes) + " MB/S"; }), shown(sample.addressCopyCapPercent, [&](double percent) { return "CAP: " + value(percent) + "%"; })}); + joined("UNIMPLEMENTED:", {shown(sample.unimplementedPerSecond, [&](double calls) { return count(calls) + "/S"; }), shown(sample.unimplementedTotal, [&](std::uint64_t calls) { return count(static_cast(calls)) + " TOTAL"; })}); + joined("REFUSED SHADERS:", {shown(sample.refusedShaders, [&](std::uint64_t refused) { return count(static_cast(refused)); })}); + joined("", {sample.title.empty() ? std::nullopt : std::optional("TITLE: " + OverlayText(sample.title)), sample.presentMode.empty() ? std::nullopt : std::optional("PRESENT: " + OverlayText(sample.presentMode))}); + return content; +} + +OverlayTraceFigures OverlayTraceDeltas(std::span before, std::span now, OverlayStatistics::Clock::duration elapsed, std::uint64_t presents, bool passes) { + using Duration = OverlayStatistics::Clock::duration; + const auto milliseconds = [](Duration length) { return std::chrono::duration(length).count(); }; + std::map busy; + std::map phases; + std::optional waits; + std::uint64_t renderPasses = 0; + for (std::size_t index = 0; index < now.size(); ++index) { + const auto& thread = now[index]; + const auto totals = TraceFormat::Since(thread.totals, index < before.size() ? std::span(before[index].totals) : std::span()); + const bool worker = std::string_view(thread.name).starts_with(TraceFormat::WorkerPrefix); + const bool firstQueue = thread.name == std::string(TraceFormat::WorkerPrefix) + "0x0"; + if (firstQueue && !waits) waits = OverlayQueueWaits{}; + for (const auto& total : totals) { + const std::string_view scope = total.scope; + const std::string_view stage = total.stage != nullptr ? total.stage : ""; + if (scope == PerformanceTrace::RenderPassScope) { + renderPasses += total.count; + continue; + } + if (worker && total.stage == nullptr && std::find(TraceFormat::WorkerBusyScopes.begin(), TraceFormat::WorkerBusyScopes.end(), scope) != TraceFormat::WorkerBusyScopes.end()) busy[thread.name] += total.length; + if (scope != TraceFormat::WorkerWaitScope) phases[TraceFormat::Name(total.scope, total.stage)] += total.length; + if (!firstQueue) continue; + if (scope == "Driver.Packet" && (stage == "memory_wait" || stage == "rendering_wait")) waits->memoryMs += milliseconds(total.length); + else if (scope == "GpuMutex.wait") waits->lockMs += milliseconds(total.length); + else if ((total.stage == nullptr && (scope == "Recorder.Sync" || scope == "Recorder.WaitSerial")) || (scope == "Graphics.Wait" && stage == "fence_wait")) waits->gpuMs += milliseconds(total.length); + } + if (worker && !busy.contains(thread.name)) busy[thread.name] = Duration{}; + } + OverlayTraceFigures figures; + if (elapsed > Duration{}) { + for (const auto& [name, length] : busy) figures.busy.push_back({busyName(name), 100.0 * static_cast(length.count()) / static_cast(elapsed.count())}); + std::stable_sort(figures.busy.begin(), figures.busy.end(), [](const OverlayThreadBusy& left, const OverlayThreadBusy& right) { return left.name.size() < right.name.size(); }); + } + if (presents == 0) return figures; + const double perFrame = 1.0 / static_cast(presents); + std::vector> top(phases.begin(), phases.end()); + std::stable_sort(top.begin(), top.end(), [](const auto& left, const auto& right) { return left.second > right.second; }); + for (std::size_t index = 0; index < top.size() && index < OverlayTopPhases; ++index) figures.phases.push_back({top[index].first, milliseconds(top[index].second) * perFrame}); + if (waits) figures.queueWaits = OverlayQueueWaits{waits->memoryMs * perFrame, waits->lockMs * perFrame, waits->gpuMs * perFrame}; + if (passes) figures.renderPassesPerFrame = static_cast(renderPasses) * perFrame; + return figures; +} + +std::optional OverlayHitPercent(const OverlayHits& before, const OverlayHits& now) { + const bool reset = now.lookups < before.lookups || now.hits < before.hits; + const auto lookups = reset ? now.lookups : now.lookups - before.lookups; + const auto hits = reset ? now.hits : now.hits - before.hits; + if (lookups == 0 || hits > lookups) return std::nullopt; + return 100.0 * static_cast(hits) / static_cast(lookups); +} + +std::string_view OverlayPresentModeName(VkPresentModeKHR mode) { + switch (mode) { + case VK_PRESENT_MODE_FIFO_KHR: return "fifo"; + case VK_PRESENT_MODE_FIFO_LATEST_READY_KHR: return "fifo_latest_ready"; + case VK_PRESENT_MODE_MAILBOX_KHR: return "mailbox"; + case VK_PRESENT_MODE_IMMEDIATE_KHR: return "immediate"; + default: return {}; + } +} + bool OverlaySampleDue(std::optional sampledAt, OverlayStatistics::Clock::time_point now) { return !sampledAt || now - *sampledAt >= std::chrono::seconds(1); } @@ -439,6 +633,51 @@ std::optional ReadProcessPrivateBytes() { #endif } +std::optional ParseCommitCharge(std::string_view meminfo) { + std::optional committed; + std::optional limit; + while (!meminfo.empty()) { + const auto end = meminfo.find('\n'); + const auto line = meminfo.substr(0, end); + meminfo = end == std::string_view::npos ? std::string_view{} : meminfo.substr(end + 1); + if (const auto value = meminfoKilobytes(line, "Committed_AS:")) committed = value; + else if (const auto value = meminfoKilobytes(line, "CommitLimit:")) limit = value; + } + if (!committed || !limit) return std::nullopt; + return OverlayMemory{*committed, *limit}; +} + +std::optional ReadCommitCharge() { +#ifdef _WIN32 + PERFORMANCE_INFORMATION information{}; + information.cb = sizeof(information); + if (K32GetPerformanceInfo(&information, sizeof(information)) == 0) return std::nullopt; + const auto page = static_cast(information.PageSize); + return OverlayMemory{static_cast(information.CommitTotal) * page, static_cast(information.CommitLimit) * page}; +#else + std::FILE* meminfo = std::fopen("/proc/meminfo", "r"); + if (meminfo == nullptr) return std::nullopt; + std::string text; + char block[1024]; + for (std::size_t read = 0; (read = std::fread(block, 1, sizeof(block), meminfo)) != 0;) text.append(block, read); + std::fclose(meminfo); + return ParseCommitCharge(text); +#endif +} + +std::optional ReadPageFaults() { +#ifdef _WIN32 + PROCESS_MEMORY_COUNTERS counters{}; + counters.cb = sizeof(counters); + if (K32GetProcessMemoryInfo(GetCurrentProcess(), &counters, sizeof(counters)) == 0) return std::nullopt; + return static_cast(counters.PageFaultCount); +#else + rusage usage{}; + if (getrusage(RUSAGE_SELF, &usage) != 0) return std::nullopt; + return static_cast(usage.ru_minflt) + static_cast(usage.ru_majflt); +#endif +} + std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content) { auto scale = std::max(1, static_cast((extent.height + 240) / 480)); while (scale > 1 && !fits(extent, content, scale)) --scale; @@ -448,10 +687,12 @@ std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content) { OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner) { Graphics::Require(extent.width != 0 && extent.height != 0, "the performance overlay needs a destination"); Graphics::Require(content.frameTimes.size() <= OverlayStatistics::FrameHistory, "the performance overlay graph holds at most 120 frames"); + Graphics::Require(content.columns > 0, "the performance overlay panel needs at least one column"); + Graphics::Require(content.graph ? content.columns * OverlayMetrics::Advance - 1 >= static_cast(OverlayStatistics::FrameHistory) : content.frameTimes.empty(), "the performance overlay graph needs a graph row as wide as its 120 frames"); OverlayLayout layout; const auto scale = OverlayScale(extent, content); layout.scale = scale; - const auto width = panelWidth(scale); + const auto width = panelWidth(content.columns, scale); const auto height = panelHeight(content, scale); const auto margin = OverlayMetrics::Margin * scale; const bool right = corner == OverlayCorner::TopRight || corner == OverlayCorner::BottomRight; @@ -461,7 +702,7 @@ OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, Ov layout.bounds = {left, top, width, height, static_cast(OverlayColor::Background)}; layout.rects.push_back(layout.bounds); const auto text = [&](const std::string& line, std::int32_t row) { - Graphics::Require(line.size() <= static_cast(OverlayMetrics::Columns), "a performance overlay line is wider than the panel"); + Graphics::Require(line.size() <= static_cast(content.columns), "a performance overlay line is wider than the panel"); for (std::size_t column = 0; column < line.size(); ++column) { const auto& glyph = OverlayGlyph(line[column]); const auto glyphLeft = left + (OverlayMetrics::Padding + static_cast(column) * OverlayMetrics::Advance) * scale; @@ -487,7 +728,7 @@ OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, Ov row += OverlayMetrics::LinePitch; } const auto graphBottom = top + (row + OverlayMetrics::GraphHeight) * scale; - if (!content.frameTimes.empty()) { + if (content.graph && !content.frameTimes.empty()) { double maximum = 0; double sum = 0; for (const auto time : content.frameTimes) { @@ -501,7 +742,7 @@ OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, Ov layout.rects.push_back({left + (OverlayMetrics::Padding + first + static_cast(i)) * scale, graphBottom - pixels, scale, pixels, static_cast(OverlayColor::Graph)}); } } - row += OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap; + if (content.graph) row += OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap; for (const auto& line : content.below) { text(line, row); row += OverlayMetrics::LinePitch; @@ -580,19 +821,39 @@ void PerformanceOverlay::release() noexcept { } bool PerformanceOverlay::Visible() { - return settings().visible != ((toggleCount.load(std::memory_order_relaxed) & 1u) != 0); + static_cast(settings()); + return currentPage.load(std::memory_order_relaxed) != 0; +} + +std::uint32_t PerformanceOverlay::Page() { + static_cast(settings()); + return currentPage.load(std::memory_order_relaxed); } void PerformanceOverlay::Toggle() { - toggleCount.fetch_add(1, std::memory_order_relaxed); + static std::mutex toggleMutex; + std::lock_guard lock(toggleMutex); + const auto presses = toggleCount.load(std::memory_order_relaxed) + 1; + const auto shown = OverlayPageAfter(settings().page, presses); + currentPage.store(shown, std::memory_order_relaxed); + toggleCount.store(presses, std::memory_order_relaxed); + PerformanceTrace::SetOverlayTotals(shown == 2); +} + +void PerformanceOverlay::SetDriverCounters(OverlayDriverCounters (*source)()) { + driverCounters.store(source, std::memory_order_release); } OverlayCorner PerformanceOverlay::Corner() { return settings().corner; } -void PerformanceOverlay::NotePresent(OverlayStatistics::Clock::time_point at, VkExtent2D display) { +void PerformanceOverlay::NotePresent(OverlayStatistics::Clock::time_point at, VkExtent2D display, const OverlayPresentInfo& info) { restartIfToggled(); + if (page == 2) { + noteDetailPresent(at, info); + return; + } if (OverlaySampleDue(sampledAt, at)) { details.vram = ReadVramBudget(context); details.ram = ReadProcessPrivateBytes(); @@ -605,13 +866,100 @@ void PerformanceOverlay::NotePresent(OverlayStatistics::Clock::time_point at, Vk void PerformanceOverlay::restartIfToggled() { const auto current = toggleCount.load(std::memory_order_relaxed); + page = Page(); if (current == toggles) return; statistics.Reset(); details = {}; + detailSample = {}; + detail = {}; sampledAt.reset(); toggles = current; } +void PerformanceOverlay::noteDetailPresent(OverlayStatistics::Clock::time_point at, const OverlayPresentInfo& info) { + PerformanceTrace::BindThread("presenter"); + statistics.AddPresent(at, OverlayCounters{0, 0, 0, 0, PerformanceCounters::Pipelines()}); + if (info.displayAddress != 0 && info.displayAddress == detail.lastDisplayAddress) ++detail.repeats; + detail.lastDisplayAddress = info.displayAddress; + ++detail.tracePresents; + detailSample.fps = statistics.Fps(); + detailSample.frameMs = statistics.LastFrameMs(); + detailSample.presentsPerSecond = statistics.Fps(); + detailSample.pipelinesPerSecond = static_cast(statistics.PipelinesInWindow()); + if (OverlaySampleDue(detail.sampledAt, at)) sampleDetails(at, info); +} + +void PerformanceOverlay::sampleDetails(OverlayStatistics::Clock::time_point at, const OverlayPresentInfo& info) { + DetailCounters now; + if (const auto source = driverCounters.load(std::memory_order_acquire)) now.driver = source(); + const auto pipelines = Graphics::PipelineStoreTotals(); + now.pipelines = OverlayHits{pipelines.hits + pipelines.misses, pipelines.hits}; + const auto copies = Graphics::AddressCopyCounters::Fold(); + now.copiedBytes = copies.bytes; + now.pageFaults = ReadPageFaults(); + now.unimplemented = NotImplementedCalls_nid_no_patch(); + now.acquireDrops = info.acquireDrops; + const auto& before = detail.base; + if (detail.sampledAt) { + const double seconds = std::chrono::duration(at - *detail.sampledAt).count(); + const auto perSecond = [&](std::uint64_t count) { return static_cast(count) / seconds; }; + if (now.driver && before.driver) { + detailSample.flipsPerSecond = perSecond(now.driver->flips - before.driver->flips); + if (now.driver->drawCache && before.driver->drawCache) detailSample.drawCacheHitPercent = OverlayHitPercent(*before.driver->drawCache, *now.driver->drawCache); + } + detailSample.pipelineCacheHitPercent = OverlayHitPercent(before.pipelines, now.pipelines); + detailSample.cpuCopyMegabytesPerSecond = perSecond(now.copiedBytes - before.copiedBytes) / (1024.0 * 1024.0); + if (const auto limit = Graphics::AddressCopyCounters::Limit(); limit != 0) detailSample.addressCopyCapPercent = 100.0 * static_cast(copies.largest) / static_cast(limit); + if (now.pageFaults && before.pageFaults) detailSample.pageFaultsPerSecond = perSecond(*now.pageFaults - *before.pageFaults); + detailSample.unimplementedPerSecond = perSecond(now.unimplemented - before.unimplemented); + detailSample.droppedFrames = static_cast(std::llround(perSecond(now.acquireDrops - before.acquireDrops))); + detailSample.repeatedPresents = static_cast(std::llround(perSecond(detail.repeats))); + } else if (HasAppParamJson_nid_no_patch()) { + const auto id = GetAppTitleId_nid_postfix(); + detailSample.title.assign(id.value, strnlen(id.value, sizeof(id.value))); + } + if (now.driver && before.driver && !now.driver->drawCache) now.driver->drawCache = before.driver->drawCache; + detailSample.unimplementedTotal = now.unimplemented; + if (const auto pending = DriverDetail::RegistrationPreparationsPending()) detailSample.pipelinesCompiling = *pending; + if (const auto refused = DriverDetail::RefusedShaderCount()) detailSample.refusedShaders = *refused; + else detailSample.refusedShaders.reset(); + detailSample.commit = ReadCommitCharge(); + if (context.hostImportAlignment != 0) { + const auto imports = Graphics::HostImportCounters(); + detailSample.hostImports = imports.live; + detailSample.hostImportBytes = OverlayMemory{imports.liveBytes, imports.liveBytes + Graphics::HostImportBudgetRemaining(context)}; + } + detailSample.presentMode = std::string(OverlayPresentModeName(info.presentMode)); + sampleTrace(at); + detail.base = std::move(now); + detail.repeats = 0; + detail.sampledAt = at; +} + +void PerformanceOverlay::sampleTrace(OverlayStatistics::Clock::time_point at) { + if (!PerformanceTrace::Recording()) { + detail.traceSampledAt.reset(); + detail.traceBase.clear(); + detailSample.busy.clear(); + detailSample.phases.clear(); + detailSample.queueWaits.reset(); + detailSample.renderPassesPerFrame.reset(); + return; + } + auto threads = PerformanceTrace::ThreadTotals(); + if (!threads) return; + if (detail.traceSampledAt) { + auto figures = OverlayTraceDeltas(detail.traceBase, *threads, at - *detail.traceSampledAt, detail.tracePresents, PerformanceTrace::OverlayTotals()); + detailSample.busy = std::move(figures.busy); + detailSample.phases = std::move(figures.phases); + detailSample.queueWaits = figures.queueWaits; + detailSample.renderPassesPerFrame = figures.renderPassesPerFrame; + } + detail.traceBase = std::move(*threads); + detail.traceSampledAt = at; + detail.tracePresents = 0; +} + VkRenderPass PerformanceOverlay::renderPass(VkFormat format, VkImageLayout initialLayout, VkImageLayout finalLayout) { const auto key = std::tuple{format, initialLayout, finalLayout}; if (const auto found = renderPasses.find(key); found != renderPasses.end()) return found->second; @@ -711,6 +1059,7 @@ VkPipeline PerformanceOverlay::pipeline(VkFormat format, VkRenderPass compatible } OverlayRect PerformanceOverlay::Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout) { + if (page == 2) return Record(commands, slot, destination, format, colorSpace, extent, initialLayout, finalLayout, ComposeOverlayDetails(detailSample), Corner()); auto shown = details; shown.window = extent; return Record(commands, slot, destination, format, colorSpace, extent, initialLayout, finalLayout, ComposeOverlay(statistics, PerformanceCounters::GpuEnabled(), PerformanceCounters::DriverEnabled(), shown), Corner()); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp index e9578a2e7c..1b1706181f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp @@ -214,7 +214,11 @@ double seconds(Clock::duration elapsed) { } void PerformanceTrace::BindThread(const char* name, std::size_t capacity) { - if (!Configured() || local != nullptr) return; + if (local != nullptr) return; + if (!Configured()) { + localName = name; + return; + } const bool windowed = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr; if (windowed && capacity == TraceThread::DefaultCapacity) capacity = TraceThread::ParseCapacity(std::getenv("APS5_TRACE_EVENTS")); auto bound = std::make_unique(name, presentCount, windowed ? capacity : 0, buffering); @@ -224,6 +228,39 @@ void PerformanceTrace::BindThread(const char* name, std::size_t capacity) { state.threads.push_back(std::move(bound)); } +TraceThread* PerformanceTrace::bindNamed() { + if (localName == nullptr) return nullptr; + auto bound = std::make_unique(localName, presentCount, 0, buffering); + auto& state = session(); + std::lock_guard lock(state.mutex); + local = bound.get(); + state.threads.push_back(std::move(bound)); + return local; +} + +void PerformanceTrace::SetOverlayTotals(bool on) { + if (Configured()) return; + binder.store(on ? &bindNamed : nullptr, std::memory_order_release); + recording.store(on, std::memory_order_relaxed); +} + +bool PerformanceTrace::OverlayTotals() { + return binder.load(std::memory_order_relaxed) != nullptr; +} + +std::optional> PerformanceTrace::ThreadTotals() { + auto& state = session(); + std::unique_lock lock(state.mutex, std::try_to_lock); + if (!lock.owns_lock()) return std::nullopt; + std::vector threads; + threads.reserve(state.threads.size()); + for (const auto& thread : state.threads) { + auto snapshot = thread->Collect(); + threads.push_back({std::move(snapshot.name), std::move(snapshot.totals)}); + } + return threads; +} + void PerformanceTrace::OpenInterval(Clock::time_point now, std::uint64_t present) { auto& state = session(); state.interval->Restart(now - state.first); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTraceThread.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTraceThread.cpp index 9ce81e5294..edfd1fa98c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTraceThread.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTraceThread.cpp @@ -4,6 +4,8 @@ namespace AgcDriver { std::atomic PerformanceTrace::recording{false}; thread_local TraceThread* PerformanceTrace::local = nullptr; +thread_local const char* PerformanceTrace::localName = nullptr; +std::atomic PerformanceTrace::binder{nullptr}; } @@ -12,5 +14,12 @@ extern "C" bool AgcDriverTraceRecording_nid_postfix() { } extern "C" AgcDriver::TraceThread* AgcDriverTraceThread_nid_postfix() { - return AgcDriver::PerformanceTrace::local; + if (auto* thread = AgcDriver::PerformanceTrace::local) return thread; + const auto bind = AgcDriver::PerformanceTrace::binder.load(std::memory_order_acquire); + return bind != nullptr ? bind() : nullptr; +} + +extern "C" void AgcDriverTraceCount_nid_postfix(const char* scope) { + if (AgcDriver::PerformanceTrace::binder.load(std::memory_order_relaxed) == nullptr) return; + if (auto* thread = AgcDriverTraceThread_nid_postfix()) thread->Add(scope, nullptr, {}, {}); } 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/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 1fac20c54d..f1f74c7206 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" @@ -344,6 +345,7 @@ struct VulkanDevice::State { std::vector images; VkFence acquireFence = VK_NULL_HANDLE; VkPresentModeKHR presentMode = VK_PRESENT_MODE_FIFO_KHR; + std::uint64_t presentedAddress = 0; struct RetiredSwapchain { VkSwapchainKHR swapchain = VK_NULL_HANDLE; std::vector rendered; @@ -369,6 +371,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}; @@ -518,6 +522,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; @@ -540,6 +545,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; @@ -741,7 +747,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); @@ -914,6 +922,7 @@ struct VulkanDevice::State { detiler.reset(); colorTransfer.reset(); overlay.reset(); + planePass.reset(); presentation.reset(); scaler.reset(); hdrScaler.reset(); @@ -945,6 +954,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) { @@ -1283,6 +1293,7 @@ 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; @@ -2095,6 +2106,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")); @@ -3199,9 +3211,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) { @@ -3332,6 +3344,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. @@ -3356,7 +3418,17 @@ FrameDumps& Dumps() { } -bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { +bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer, const DisplayBuffer* plane) { + state->presentedAddress = buffer.address; + 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"); @@ -3379,7 +3451,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; @@ -3411,19 +3483,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; } } @@ -3454,7 +3514,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(); @@ -3510,7 +3570,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"); @@ -3544,6 +3604,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)); @@ -3588,6 +3654,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); @@ -3635,7 +3729,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); @@ -3656,13 +3750,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()); } } @@ -3673,11 +3771,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->NotePresent(std::chrono::steady_clock::now(), clearOnly ? VkExtent2D{} : VkExtent2D{width, height}, OverlayPresentInfo{std::exchange(state->presentedAddress, 0), state->presentMode, presentCounters.acquireDrops}); + 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; @@ -3705,6 +3813,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; @@ -3720,6 +3829,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 @@ -3977,7 +4087,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, diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp index 580236b120..3f75b2a804 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "Recompiler.hpp" +#include #include #include #include @@ -22,7 +23,7 @@ public: std::uint32_t stage = 0; std::uint64_t shader = 0; std::vector poison; - std::vector messages; + std::vector> messages; }; void SetImageTables(std::vector tables, std::vector> ranges); struct SrtGuardFaults { 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 bc6afff426..4d29d617fb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -6,6 +6,7 @@ #include "prx/libc/include/GuestAllocations.hpp" #include #include +#include #include #include #include @@ -75,8 +76,8 @@ ImportProbe ProbeDmaBufImportWriteProtection(const Context& context); ImportWatch PrepareImportWatch(const Context& context); void SetImportWatch(const Context& context, ImportWatch watch); -enum class ImportRequester : std::uint8_t { Map, Build, Access, Join, Segment }; -inline constexpr std::size_t ImportRequesterCount = 5; +enum class ImportRequester : std::uint8_t { Map, Build, Access, Join, Segment, ColorTarget }; +inline constexpr std::size_t ImportRequesterCount = 6; struct HostImportRequesterCounts { std::uint64_t made = 0; @@ -135,6 +136,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; @@ -271,6 +273,24 @@ struct AddressCopy { }; std::string AddressCopyOverflow(std::vector copies, std::uint64_t limit); +class AddressCopyCounters { +public: + struct Totals { + std::uint64_t bytes = 0; + std::uint64_t largest = 0; + }; + + static AddressCopyCounters& ThisThread(); + static Totals Fold(); + static std::uint64_t Limit(); + + void Count(std::uint64_t copied); + +private: + alignas(64) std::atomic bytes{0}; + std::atomic largest{0}; +}; + class GuestBufferMemory { public: explicit GuestBufferMemory(const Context& context); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 2438745430..a2c9a54ea5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -401,7 +401,7 @@ private: std::size_t mapAllocation = std::numeric_limits::max(); std::vector map; std::vector poison; - std::vector messages; + std::vector> messages; std::uint32_t elementBase = 0; std::uint32_t set = 0; std::size_t firstSampler = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 294c4bda9b..13d227b8ea 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -181,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 85a45ae60e..7b7b420c3c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -65,7 +65,7 @@ public: // compute pass writing it and the next pass sampling it share one image and copy nothing. // CanCopyFrom says whether the two descriptors address the same surface compatibly. Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components); - Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, std::uint32_t baseLayer = 0, std::shared_ptr owner = {}, VkImageUsageFlags usage = 0); + Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, std::uint32_t baseLayer = 0, std::shared_ptr owner = {}, VkImageUsageFlags usage = 0, std::uint32_t layerCount = VK_REMAINING_ARRAY_LAYERS); struct DepthCopyTag {}; Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag); struct SrgbExpandTag {}; 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/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index aa267a27f6..a8440fb7f0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -272,7 +272,7 @@ void BdaResources::CheckFault() const { message << std::dec << ")"; } message << ": " << ShaderRecompiler::ImageTableAbi::PoisonReasonName(static_cast(poison.reason)); - if (report.bytes < tables->messages.size() && !tables->messages[report.bytes].empty()) message << ": " << tables->messages[report.bytes]; + if (report.bytes < tables->messages.size() && tables->messages[report.bytes] != nullptr && !tables->messages[report.bytes]->empty()) message << ": " << *tables->messages[report.bytes]; } else { message << ": poison entry " << report.bytes; } 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 e7189982e9..b8084357b1 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) { @@ -1170,7 +1190,6 @@ struct DepthPlaneSlice { class DepthPlaneCopy { public: DepthPlaneCopy(const Context& context, VkExtent2D extent, VkFormat format, std::uint32_t layers) : context(context), extent(extent), format(format), layers(layers), guestLayers(layers), surfaceLayers(layers) { - this->context.bufferPool.reset(); VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; info.flags = layers % 6u == 0 && extent.width == extent.height ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u; info.imageType = VK_IMAGE_TYPE_2D; @@ -1185,15 +1204,16 @@ public: info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; try { Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage depth plane copy"); + owned = std::make_shared(context, image, VK_NULL_HANDLE); VkMemoryRequirements requirements{}; context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory depth plane copy"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory depth plane copy"); + memory = owned->memory; Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory depth plane copy"); imageBytes = requirements.size; - owned = std::make_shared(context, image, memory); } catch (...) { release(); throw; @@ -1204,19 +1224,21 @@ public: DepthPlaneCopy& operator=(const DepthPlaneCopy&) = delete; std::shared_ptr Refresh(std::span slices, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer) { - Require(slices.size() == layers, "depth plane copy slices do not match its layers"); + const auto requested = static_cast(slices.size()); + Require(requested != 0 && requested <= layers, "depth plane copy slices exceed its layers"); + Require(baseLayer < requested, "a depth plane copy view starts past the slices it was given"); const auto geometry = DescribeSurface(resource); const bool guestFill = std::any_of(slices.begin(), slices.end(), [](const auto& slice) { return slice.image == VK_NULL_HANDLE; }); - if (guestFill && (geometry.layers != layers || geometry.sliceLinearBytes < sliceBytes() || geometry.mips.empty())) { + if (guestFill && (geometry.layers != requested || geometry.sliceLinearBytes < sliceBytes() || geometry.mips.empty())) { char text[320]; std::snprintf(text, sizeof(text), "AGC graphics: depth plane copy geometry does not match its layers (copy %ux%u, %u layers, %llu bytes a slice; texture %ux%u guest format %u, dimension %d, tile mode %u, %u layers, %llu bytes a slice, %zu mips)", extent.width, extent.height, layers, static_cast(sliceBytes()), resource.width, resource.height, resource.format, static_cast(resource.dimension), resource.tileMode, geometry.layers, static_cast(geometry.sliceLinearBytes), geometry.mips.size()); throw std::runtime_error(text); } - const auto stagingBytes = std::max(static_cast(geometry.sliceLinearBytes), static_cast(sliceBytes())) * layers; + const auto stagingBytes = std::max(static_cast(geometry.sliceLinearBytes), static_cast(sliceBytes())) * requested; std::vector> uploads; std::vector uploadLayers; std::vector copiedLayers; - for (std::uint32_t layer = 0; layer < layers; ++layer) { + for (std::uint32_t layer = 0; layer < requested; ++layer) { auto& held = guestLayers[layer]; auto& surface = surfaceLayers[layer]; if (slices[layer].image != VK_NULL_HANDLE) { @@ -1244,10 +1266,11 @@ public: if (staging == nullptr || stagingBytes > staging->Size()) staging = std::make_shared(context, stagingBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); record(slices, resource, geometry, copiedLayers, uploads, uploadLayers, staging); } - const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a), static_cast(viewType), baseLayer}; + const auto viewLayers = viewType == VK_IMAGE_VIEW_TYPE_2D ? 1u : requested - baseLayer; + const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a), static_cast(viewType), baseLayer, viewLayers}; auto& texture = textures[key]; if (texture == nullptr) { - texture = std::make_shared(context, image, format, aspect(), components, viewType, baseLayer, owned); + texture = std::make_shared(context, image, format, aspect(), components, viewType, baseLayer, owned, 0, viewLayers); texture->MarkRefreshedPerUse(); } return texture; @@ -1275,7 +1298,7 @@ private: VkDeviceMemory memory = VK_NULL_HANDLE; std::shared_ptr owned; std::shared_ptr staging; - std::map, std::shared_ptr> textures; + std::map, std::shared_ptr> textures; std::vector guestLayers; std::vector surfaceLayers; bool filled = false; @@ -1365,8 +1388,8 @@ private: } }; -std::map, std::unique_ptr>& planeCopies() { - static auto* copies = new std::map, std::unique_ptr>(); +std::map, std::unique_ptr>& planeCopies() { + static auto* copies = new std::map, std::unique_ptr>(); return *copies; } @@ -1375,7 +1398,7 @@ std::uint64_t& planeCopyClock() { return clock; } -void evictPlaneCopies(const std::tuple& keep) { +void evictPlaneCopies(const std::tuple& keep) { static const VkDeviceSize budget = [] { const char* text = std::getenv("APS5_DEPTH_PLANE_COPY_MIB"); return static_cast(text != nullptr ? std::strtoull(text, nullptr, 10) : 256ull) << 20u; @@ -2399,9 +2422,9 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::span copyKey{context.device, base.address, base.extent.width, base.extent.height, imageFormat, layers}; + const std::tuple copyKey{context.device, base.address, base.extent.width, base.extent.height, imageFormat}; auto& copy = copies[copyKey]; - if (copy == nullptr) copy = std::make_unique(context, base.extent, imageFormat, layers); + if (copy == nullptr || copy->layers < layers) copy = std::make_unique(context, base.extent, imageFormat, layers); copy->lastUse = ++planeCopyClock(); auto texture = copy->Refresh(slices, resource, components, cube || resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, layered && !cube ? resource.baseArray : 0u); evictPlaneCopies(copyKey); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index f3dd7f8885..967d113df9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2253,7 +2253,10 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam outcome.passContinued = continued; outcome.passBegun = !continued; const auto commands = start.commands; - if (!continued) recorder->PrepareSampleSlot(); + if (!continued) { + recorder->PrepareSampleSlot(); + PerformanceTrace::Count(PerformanceTrace::RenderPassScope); + } if (capture) captureInputs(context, *recorder, commands, resources, drawBindings, record.targets.empty() || record.targets.front() == nullptr ? 0 : record.targets.front()->Descriptor().baseAddress); // The draw's [gputime] class range: from its first barrier to the pass's trailing barrier (a // continued draw lies inside its pass's range). @@ -2566,7 +2569,7 @@ bool prepareDraw(const Context& context, const State& state, const Pm4::DrawPara // Debug aid: APS5_NO_RESIDENT_TARGETS=1 copies every target in and out again. static const bool residentTargets = std::getenv("APS5_NO_RESIDENT_TARGETS") == nullptr; static const bool residentLinear = std::getenv("APS5_NO_RESIDENT_LINEAR") == nullptr; - const bool linearResident = residentLinear && color.tileMode == ColorTileMode::Linear && ExistingHostImportFor(context, color.address, colorLayout.Bytes()) != nullptr; + const bool linearResident = residentLinear && residentTargets && context.detiler != nullptr && color.tileMode == ColorTileMode::Linear && (ExistingHostImportFor(context, color.address, colorLayout.Bytes()) != nullptr || (GuestMemory::Watched(color.address, colorLayout.Bytes()) && HostImportFor(context, color.address, colorLayout.Bytes(), ImportRequester::ColorTarget) != nullptr)); if ((binding.gpuTiling || ((color.tileMode != ColorTileMode::Linear || linearResident) && context.detiler != nullptr)) && residentTargets) { // The lookup refreshes the image on every draw (StorageTexture::Refresh: FlushPending, // CollectWrites over the target's pages, the DCC key scan of TextureClearKeys, then diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 6ade9b851a..38886bd9c8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -179,6 +180,14 @@ struct ImportRefusal { std::uint64_t budget; }; +struct GateRefusal { + std::uint64_t base; + std::uint64_t bytes; + GuestAllocations::Mapped ranges; + std::uint64_t commits; + std::uint64_t watches; +}; + struct EvictingRefusal { std::uint64_t base; std::uint64_t bytes; @@ -250,6 +259,7 @@ struct HostImports { std::map, ImportFailure> failedBlocks; std::map, ImportRefusal> refusals; std::map, EvictingRefusal> evictingRefusals; + std::map, GateRefusal> gateRefusals; // Registry generation the imports were last reconciled with. std::uint64_t refreshedGeneration = 0; // Bumped whenever an import is dropped, so HostImport pointers taken under the lock earlier can be @@ -287,6 +297,10 @@ struct HostImports { std::uint8_t reportNoted = 0; std::map spaceUses; std::uint64_t budgetRefusals = 0; + std::array recentImports{}; + std::uint64_t recentEpoch = std::numeric_limits::max(); + std::size_t recentSize = 0; + std::size_t recentNext = 0; }; HostImports& Imports() { @@ -294,6 +308,31 @@ HostImports& Imports() { return imports; } +bool recentImportsEnabled() { + static const bool enabled = std::getenv("APS5_NO_RECENT_IMPORTS") == nullptr; + return enabled; +} + +const HostImport* recentImport(HostImports& state, std::uint64_t begin, std::uint64_t end) { + if (state.recentEpoch != state.epoch || state.recentSize != state.imports.size()) return nullptr; + for (const auto* entry : state.recentImports) { + if (entry != nullptr && begin >= entry->base && end <= entry->base + entry->bytes) { + entry->lastUse = ++state.useCounter; + return entry; + } + } + return nullptr; +} + +void rememberImport(HostImports& state, const HostImport* entry) { + if (state.recentEpoch != state.epoch || state.recentSize != state.imports.size()) { + state.recentImports = {}; + state.recentEpoch = state.epoch; + state.recentSize = state.imports.size(); + } + state.recentImports[state.recentNext++ % state.recentImports.size()] = entry; +} + #ifdef _WIN32 void waitSettleBatch(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { if (batch.busy && settle.waitForFences != nullptr) Check(settle.waitForFences(device, 1, &batch.fence, VK_TRUE, 60'000'000'000ull), "vkWaitForFences (settle batch)"); @@ -828,8 +867,12 @@ struct BackingFile { std::uint64_t offset = 0; }; +bool sharedBacking(const Context& context, std::uint64_t base, std::uint64_t bytes, BackingFile& backing) { + return context.dmaBufImport && GuestArena::GuestArenaSharedBacking_nid_postfix(static_cast(base), static_cast(bytes), &backing.file, &backing.offset); +} + bool sharedBacking(const Context& context, const HostImport& entry, BackingFile& backing) { - return context.dmaBufImport && GuestArena::GuestArenaSharedBacking_nid_postfix(static_cast(entry.base), static_cast(entry.bytes), &backing.file, &backing.offset); + return sharedBacking(context, entry.base, entry.bytes, backing); } const char* createBackingImport(const Context& context, HostImport& entry, BackingFile& backing, VkResult& failure) { @@ -1252,6 +1295,62 @@ void decideImportWatch(const Context& context, HostImports& state) { #endif } +struct ImportWatching { + bool settled = false; + bool hostPointer = false; + bool dmaBuf = false; +}; + +ImportWatching importWatching(const Context& context, HostImports& state, bool alias, bool writable, std::uint64_t bytes) { + decideImportWatch(context, state); +#ifdef _WIN32 + const bool settled = !alias && state.watchPrivateImports && writable && bytes >= settleFloorBytes; + const bool watched = alias || !state.unwatchImports || settled; + return {settled, watched, watched}; +#else + static_cast(alias); + static_cast(writable); + static_cast(bytes); + return {false, !state.unwatchImports, !state.unwatchDmaBufImports}; +#endif +} + +#ifdef _WIN32 +struct ImportPages { + bool committed = true; + bool writable = true; + bool readOnly = true; + bool shared = true; + MEMORY_BASIC_INFORMATION refused{}; +}; + +bool writableProtection(std::uint32_t protection) { + protection &= 0xffu; + return protection == PAGE_READWRITE || protection == PAGE_EXECUTE_READWRITE || protection == PAGE_WRITECOPY || protection == PAGE_EXECUTE_WRITECOPY; +} + +ImportPages importPages(std::uint64_t base, std::uint64_t bytes, bool opening = false) { + ImportPages pages; + for (std::uint64_t cursor = base; cursor < base + bytes;) { + MEMORY_BASIC_INFORMATION info{}; + if (VirtualQuery(reinterpret_cast(cursor), &info, sizeof(info)) == 0 || info.State != MEM_COMMIT || (info.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) { + pages.committed = false; + return pages; + } + const auto protection = info.Protect & 0xffu; + const bool pageWritable = (info.Type == MEM_PRIVATE || info.Type == MEM_IMAGE) && writableProtection(protection); + const bool pageReadOnly = info.Type != MEM_MAPPED && (protection == PAGE_READONLY || protection == PAGE_EXECUTE_READ); + if (!pageReadOnly) pages.readOnly = false; + if (info.Type != MEM_MAPPED) pages.shared = false; + if (info.Type != MEM_MAPPED && !pageWritable && !pageReadOnly && pages.refused.BaseAddress == nullptr) pages.refused = info; + if (!pageWritable) pages.writable = false; + if (opening && !pages.writable && !pages.shared) return pages; + cursor = reinterpret_cast(info.BaseAddress) + info.RegionSize; + } + return pages; +} +#endif + std::shared_ptr leasedRangeOwner(const GuestAllocations::Lease& lease, std::uint64_t base) { const auto found = std::lower_bound(lease.begin(), lease.end(), base, [](const auto& range, std::uint64_t value) { return range->address < value; }); return found != lease.end() && (*found)->address == base ? *found : nullptr; @@ -1270,6 +1369,22 @@ struct ImportRun { std::vector> ranges; }; +bool importStaysWatched(const Context& context, HostImports& state, const ImportRun& run) { + if (std::any_of(run.ranges.begin(), run.ranges.end(), [](const auto& range) { return !range->writable; })) return false; + if (!GuestMemory::Watched(run.base, static_cast(run.bytes))) return false; +#ifdef _WIN32 + MEMORY_BASIC_INFORMATION first{}; + if (!importWatching(context, state, false, true, run.bytes).hostPointer && (VirtualQuery(reinterpret_cast(run.base), &first, sizeof(first)) == 0 || first.Type != MEM_MAPPED)) return false; + const auto pages = importPages(run.base, run.bytes, true); + if (!pages.committed || (!pages.writable && !pages.shared)) return false; + return importWatching(context, state, !pages.writable, pages.writable, run.bytes).hostPointer; +#else + const auto watching = importWatching(context, state, false, true, run.bytes); + BackingFile backing; + return watching.hostPointer && (watching.dmaBuf || !sharedBacking(context, run.base, run.bytes, backing)); +#endif +} + bool continuesRun(const GuestAllocations::Range& before, const GuestAllocations::Range& after) { return before.address + before.bytes == after.address && before.readable && after.readable && before.writable == after.writable && before.gpu == after.gpu; } @@ -1493,8 +1608,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) { @@ -1502,8 +1641,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; } @@ -1522,6 +1663,41 @@ std::uint64_t physicalMemoryBytes() { std::atomic budgetOverride{0}; std::atomic addressCopyLimitOverride{0}; +std::uint64_t addressCopyLimit() { + static const std::uint64_t configured = [] { + 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); + return limit != 0 ? limit : configured; +} + +constexpr unsigned AddressCopyEpochShift = 48; +constexpr std::uint64_t AddressCopyEpochMask = 0xffff; +constexpr std::uint64_t AddressCopyLargestMask = (std::uint64_t{1} << AddressCopyEpochShift) - 1; +std::atomic addressCopyEpoch{0}; + +struct AddressCopyRegistry { + std::mutex mutex; + std::deque counters; + std::vector unused; +}; + +AddressCopyRegistry& addressCopyRegistry() { + static auto* registry = new AddressCopyRegistry; + return *registry; +} + +struct AddressCopyLease { + AddressCopyCounters* counters = nullptr; + ~AddressCopyLease() { + if (counters == nullptr) return; + auto& registry = addressCopyRegistry(); + std::lock_guard lock(registry.mutex); + registry.unused.push_back(counters); + } +}; + // Pinned imports count against the driver's system memory heap, which imports, type-3 mirrors, buffers and textures // placed in host memory share; past it ordinary host allocations fail. The budget is APS5_HOST_IMPORT_MIB, else // HostImportBudget. @@ -1817,6 +1993,21 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import } state.evictingRefusals.erase(remembered); } + if (requester == ImportRequester::ColorTarget) { +#ifdef _WIN32 + const auto commits = GuestArena::GuestArenaCommitGeneration_nid_postfix(); +#else + const std::uint64_t commits = 0; +#endif + const auto watches = GuestMemory::UnwatchSerial(); + const std::pair key{base, bytes}; + if (const auto refused = state.gateRefusals.find(key); refused != state.gateRefusals.end() && refused->second.commits == commits && refused->second.watches == watches && sameRun(refused->second, run)) return nullptr; + if (!importStaysWatched(context, state, run)) { + state.gateRefusals.insert_or_assign(key, GateRefusal{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end()), commits, watches}); + return nullptr; + } + state.gateRefusals.erase(key); + } auto first = state.imports.lower_bound(base); if (first != state.imports.begin() && std::prev(first)->second.base + std::prev(first)->second.bytes > base) --first; std::uint64_t replaced = 0; @@ -1891,27 +2082,17 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import #ifdef _WIN32 // Drivers pin imported pages, so every page must be committed and accessible. const GuestArena::HostImportAccess importAccess(reinterpret_cast(base), static_cast(bytes)); - bool writable = true; - bool readOnly = true; - MEMORY_BASIC_INFORMATION refused{}; - for (std::uint64_t cursor = base; cursor < base + bytes;) { - MEMORY_BASIC_INFORMATION info{}; - if (VirtualQuery(reinterpret_cast(cursor), &info, sizeof(info)) == 0 || info.State != MEM_COMMIT || (info.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) return failedImport(); - const auto protection = info.Protect & 0xffu; - const bool pageWritable = (info.Type == MEM_PRIVATE || info.Type == MEM_IMAGE) && (protection == PAGE_READWRITE || protection == PAGE_EXECUTE_READWRITE || protection == PAGE_WRITECOPY || protection == PAGE_EXECUTE_WRITECOPY); - const bool pageReadOnly = info.Type != MEM_MAPPED && (protection == PAGE_READONLY || protection == PAGE_EXECUTE_READ); - if (!pageReadOnly) readOnly = false; - if (info.Type != MEM_MAPPED && !pageWritable && !pageReadOnly && refused.BaseAddress == nullptr) refused = info; - if (!pageWritable) writable = false; - cursor = reinterpret_cast(info.BaseAddress) + info.RegionSize; - } - if (!writable && readOnly) return failedImport(); + const auto pages = importPages(base, bytes); + if (!pages.committed) return failedImport(); + const bool writable = pages.writable; + if (!writable && pages.readOnly) return failedImport(); if (!writable) { - if (refused.BaseAddress != nullptr) { + if (pages.refused.BaseAddress != nullptr) { char text[256]; - std::snprintf(text, sizeof(text), "AGC graphics: host import of 0x%llx+0x%llx: memory at 0x%llx (type 0x%lx, protection 0x%lx) is neither read-write, read-only nor a shared mapping", static_cast(base), static_cast(bytes), reinterpret_cast(refused.BaseAddress), refused.Type, refused.Protect); + std::snprintf(text, sizeof(text), "AGC graphics: host import of 0x%llx+0x%llx: memory at 0x%llx (type 0x%lx, protection 0x%lx) is neither read-write, read-only nor a shared mapping", static_cast(base), static_cast(bytes), reinterpret_cast(pages.refused.BaseAddress), pages.refused.Type, pages.refused.Protect); throw std::runtime_error(text); } + std::erase_if(state.gateRefusals, [&](const auto& refusal) { return refusal.first.first < base + bytes && base < refusal.first.first + refusal.first.second; }); try { entry.alias = GuestArena::GuestArenaMapAlias_nid_postfix(static_cast(base), static_cast(bytes)); } catch (const std::runtime_error&) { @@ -1923,17 +2104,16 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import #else if (!GuestMemory::Accessible(reinterpret_cast(base), static_cast(bytes), true)) return failedImport(); #endif - decideImportWatch(context, state); VkResult result = VK_SUCCESS; const char* step = nullptr; #ifdef _WIN32 - const bool settled = entry.alias == nullptr && state.watchPrivateImports && writable && bytes >= settleFloorBytes; - const bool watchedHostPointer = entry.alias != nullptr || !state.unwatchImports || settled; - const bool watchedDmaBuf = watchedHostPointer; + const auto watching = importWatching(context, state, entry.alias != nullptr, writable, bytes); + const bool settled = watching.settled; #else - const bool watchedHostPointer = !state.unwatchImports; - const bool watchedDmaBuf = !state.unwatchDmaBufImports; + const auto watching = importWatching(context, state, false, true, bytes); #endif + const bool watchedHostPointer = watching.hostPointer; + const bool watchedDmaBuf = watching.dmaBuf; GuestMemory::ImportWatched(base, bytes, [&] { step = watchedDmaBuf == watchedHostPointer ? createImport(context, entry, result) : createHostPointerImport(context, entry, result); #ifdef _WIN32 @@ -2063,6 +2243,7 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc state.failedBlocks.clear(); state.refusals.clear(); state.evictingRefusals.clear(); + state.gateRefusals.clear(); state.ceiling = 0; state.allocationFailures = 0; state.allocationFailed = false; @@ -2081,6 +2262,7 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); if (generation == state.refreshedGeneration) return; state.refreshedGeneration = generation; + state.gateRefusals.clear(); const auto starts = [&](std::uint64_t address) { const auto index = leasedIndexContaining(lease, address); return index != lease.size() && lease[index]->address == address; @@ -2088,6 +2270,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); @@ -2097,9 +2280,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); @@ -3459,6 +3644,7 @@ void ClearHostImports(VkDevice device) { state.failedBlocks.clear(); state.refusals.clear(); state.evictingRefusals.clear(); + state.gateRefusals.clear(); state.ceiling = 0; state.allocationFailures = 0; state.allocationFailed = false; @@ -3682,6 +3868,7 @@ void SetImportWatch(const Context& context, ImportWatch watch) { state.watchDevice = context.device; state.unwatchImports = watch == ImportWatch::Unwatch; state.unwatchDmaBufImports = state.unwatchImports; + state.gateRefusals.clear(); } void SetHostImportWindow(std::uint64_t bytes) { @@ -3725,6 +3912,47 @@ void SetAddressCopyLimit(std::uint64_t bytes) { addressCopyLimitOverride.store(bytes, std::memory_order_relaxed); } +AddressCopyCounters& AddressCopyCounters::ThisThread() { + auto& lease = HostThreadLocal(); + if (lease.counters == nullptr) { + auto& registry = addressCopyRegistry(); + std::lock_guard lock(registry.mutex); + if (registry.unused.empty()) { + lease.counters = ®istry.counters.emplace_back(); + } else { + lease.counters = registry.unused.back(); + registry.unused.pop_back(); + } + } + return *lease.counters; +} + +void AddressCopyCounters::Count(std::uint64_t copied) { + bytes.store(bytes.load(std::memory_order_relaxed) + copied, std::memory_order_relaxed); + const auto epoch = addressCopyEpoch.load(std::memory_order_relaxed) & AddressCopyEpochMask; + const auto kept = largest.load(std::memory_order_relaxed); + const auto clamped = std::min(copied, AddressCopyLargestMask); + if ((kept >> AddressCopyEpochShift) != epoch || (kept & AddressCopyLargestMask) < clamped) largest.store(epoch << AddressCopyEpochShift | clamped, std::memory_order_relaxed); +} + +AddressCopyCounters::Totals AddressCopyCounters::Fold() { + auto& registry = addressCopyRegistry(); + std::lock_guard lock(registry.mutex); + const auto epoch = addressCopyEpoch.load(std::memory_order_relaxed); + Totals totals; + for (const auto& counters : registry.counters) { + totals.bytes += counters.bytes.load(std::memory_order_relaxed); + const auto kept = counters.largest.load(std::memory_order_relaxed); + if ((kept >> AddressCopyEpochShift) == (epoch & AddressCopyEpochMask)) totals.largest = std::max(totals.largest, kept & AddressCopyLargestMask); + } + addressCopyEpoch.store(epoch + 1, std::memory_order_relaxed); + return totals; +} + +std::uint64_t AddressCopyCounters::Limit() { + return addressCopyLimit(); +} + void SetHostImportDeferredSettles(bool enabled) { #ifdef _WIN32 deferredSettles.store(enabled, std::memory_order_relaxed); @@ -3763,6 +3991,7 @@ const char* ImportRequesterName(ImportRequester requester) { case ImportRequester::Access: return "access"; case ImportRequester::Join: return "join"; case ImportRequester::Segment: return "segment"; + case ImportRequester::ColorTarget: return "color target"; } return "unknown"; } @@ -3823,8 +4052,15 @@ const HostImport* HostImportFor(const Context& context, std::uint64_t address, s #endif // A hit is only valid while the registry has not changed since the imports were reconciled. if (!importsStale(context, state)) { + if (recentImportsEnabled()) { + if (const auto* entry = recentImport(state, address, address + bytes)) { + entry->lastUse = ++state.useCounter; + return entry; + } + } if (const auto* entry = findImport(state, address, address + bytes)) { entry->lastUse = ++state.useCounter; + if (recentImportsEnabled()) rememberImport(state, entry); return entry; } } @@ -3921,6 +4157,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 { @@ -4192,6 +4443,11 @@ void GuestBufferMemory::AcquireRegistered() { } releaseImportedMirrors(context, imported); regionsSorted = sortedLookup; + if (!copies.empty()) { + std::uint64_t copied = 0; + for (const auto& copy : copies) copied += copy.committed; + AddressCopyCounters::ThisThread().Count(copied); + } if (const auto overflow = AddressCopyOverflow(std::move(copies), cap); !overflow.empty()) { std::fprintf(stderr, "FATAL: %s\n", overflow.c_str()); std::fflush(stderr); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index b4759d8ee0..7cb1eafdad 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -332,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) { @@ -535,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); @@ -548,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; } @@ -574,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); - Require(WatchedAfterSettle(context, range.address, range.bytes), "initial multisampled DCC contents require watched guest memory"); + 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) { @@ -593,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(WatchedAfterSettle(context, 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; } @@ -623,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(WatchedAfterSettle(context, 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"); } } 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 48d6b317ae..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" @@ -3679,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 88c074e25c..4420f9ecb5 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)}; } @@ -803,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); @@ -1189,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; @@ -2084,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; @@ -5025,7 +5026,7 @@ ShaderResources::TableStage& ShaderResources::tableStage(const CompiledShader& s table.stage = shader.stage; table.program = shader.program; table.poison = shader.program->imageTablePoison; - table.messages.assign(table.poison.size(), std::string{}); + table.messages.assign(table.poison.size(), nullptr); return table; } @@ -5036,7 +5037,8 @@ std::uint32_t ShaderResources::rejectTableElement(TableStage& table, std::span(ShaderRecompiler::ImageTableAbi::PoisonReason::DriverRejected); table.poison.push_back(poison); - table.messages.push_back(message); + const bool repeated = !table.messages.empty() && table.messages.back() != nullptr && *table.messages.back() == message; + table.messages.push_back(repeated ? table.messages.back() : std::make_shared(message)); faultChecks = true; return ShaderRecompiler::ImageTableAbi::PoisonCode(static_cast(table.poison.size() - 1u)); } @@ -5331,7 +5333,7 @@ void ShaderResources::finishImageTables() { poison.resource = index; poison.reason = static_cast(Abi::PoisonReason::HeapWritten); table.poison.push_back(poison); - table.messages.push_back("the table at 0x" + HexText(base) + " is written by its own dispatch"); + table.messages.push_back(std::make_shared("the table at 0x" + HexText(base) + " is written by its own dispatch")); map[header + Abi::TableFault] = Abi::PoisonCode(static_cast(table.poison.size() - 1u)); faultChecks = true; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index da78c29b18..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"); @@ -1917,6 +1979,12 @@ GuestTextureResource SurfaceForTarget(const ColorTarget& color) { 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"); @@ -2197,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 ada06eab1e..48aa6481ec 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1087,7 +1087,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& } } -Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer, std::shared_ptr owner, VkImageUsageFlags usage) : context(context), owned(std::move(owner)) { +Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer, std::shared_ptr owner, VkImageUsageFlags usage, std::uint32_t layerCount) : context(context), owned(std::move(owner)) { Require(owned == nullptr || (owned->image == depthImage && owned->context.device == context.device), "a depth sampled view must retain its own device and image"); layout = VK_IMAGE_LAYOUT_GENERAL; VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; @@ -1099,7 +1099,7 @@ Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthForma viewInfo.format = depthFormat; viewFormat = depthFormat; viewInfo.components = components; - viewInfo.subresourceRange = {aspect, 0, 1, baseLayer, viewType == VK_IMAGE_VIEW_TYPE_2D ? 1u : VK_REMAINING_ARRAY_LAYERS}; + viewInfo.subresourceRange = {aspect, 0, 1, baseLayer, viewType == VK_IMAGE_VIEW_TYPE_2D ? 1u : layerCount}; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth plane"); viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; if (viewType != VK_IMAGE_VIEW_TYPE_2D_ARRAY) return; 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/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index db50119e18..1a18c62c12 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #ifndef _WIN32 @@ -851,7 +852,7 @@ private: const auto* imported = reinterpret_cast(next); active->calls.push_back({imported->pHostPointer, info->allocationSize}); if (active->onImport) { - auto callback = std::move(active->onImport); + const auto callback = std::exchange(active->onImport, nullptr); callback(); } if (active->failure != VK_SUCCESS) return active->failure; @@ -1130,7 +1131,7 @@ void importAtMapOverBudgetTests(const Context& context) { void importRequesterTests(const Context& context, const BdaTestAccess& access) { ClearHostImports(context.device); constexpr std::size_t piece = 65536; - constexpr std::size_t pieces = 8; + constexpr std::size_t pieces = 11; auto* block = static_cast(::operator new(pieces * piece, std::align_val_t{piece})); std::memset(block, 0x2d, pieces * piece); auto importing = context; @@ -1144,7 +1145,7 @@ void importRequesterTests(const Context& context, const BdaTestAccess& access) { }; const auto counted = [](ImportRequester requester) { return HostImportCounters().requesters[static_cast(requester)]; }; const auto made = [&](ImportRequester requester) { return counted(requester).made; }; - Require(std::string(ImportRequesterName(ImportRequester::Map)) == "map" && std::string(ImportRequesterName(ImportRequester::Build)) == "build" && std::string(ImportRequesterName(ImportRequester::Access)) == "access" && std::string(ImportRequesterName(ImportRequester::Join)) == "join" && std::string(ImportRequesterName(ImportRequester::Segment)) == "segment", "an import requester has the wrong name"); + Require(std::string(ImportRequesterName(ImportRequester::Map)) == "map" && std::string(ImportRequesterName(ImportRequester::Build)) == "build" && std::string(ImportRequesterName(ImportRequester::Access)) == "access" && std::string(ImportRequesterName(ImportRequester::Join)) == "join" && std::string(ImportRequesterName(ImportRequester::Segment)) == "segment" && std::string(ImportRequesterName(ImportRequester::ColorTarget)) == "color target", "an import requester has the wrong name"); const auto mappedRange = [&](std::size_t index) { GuestAllocations::Mapped ranges; const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); @@ -1245,9 +1246,27 @@ void importRequesterTests(const Context& context, const BdaTestAccess& access) { SetHostImportWindow(0); ::operator delete(joined, std::align_val_t{window}); } + change(10, true); + change(9, true); + { + const auto decided = PrepareImportWatch(importing); + const auto before = counted(ImportRequester::ColorTarget); + SetImportWatch(importing, ImportWatch::Unwatch); + const auto* compared = HostImportFor(importing, address(10), 16, ImportRequester::ColorTarget); + const auto refused = counted(ImportRequester::ColorTarget); + SetImportWatch(importing, ImportWatch::Watch); + const auto* readOnly = HostImportFor(importing, address(9), 16, ImportRequester::ColorTarget); + const auto* outside = HostImportFor(importing, address(10), 16, ImportRequester::ColorTarget); + SetImportWatch(importing, decided); + Require(compared == nullptr && refused.made == before.made && refused.refused == before.refused && refused.failed == before.failed, "a color target's import was made where imports of its memory are compared"); + Require(readOnly == nullptr && !HostImportCovers(importing, address(9), piece), "a color target's import was made of registered memory the guest cannot write"); + Require(outside == nullptr && !HostImportCovers(importing, address(10), piece) && made(ImportRequester::ColorTarget) == before.made, "a color target's import was made of memory outside the write watch"); + } const auto counts = HostImportCounters(); Require(counts.live >= 4 && counts.liveBytes == HostImportLiveBytes(), "the live import counts disagree with the live bytes"); for (std::size_t index = 0; index < 6; ++index) change(index, false); + change(10, false); + change(9, false); { GuestAllocations::Mutation mutation; mutation.Remove(block + 6 * piece); @@ -1263,8 +1282,9 @@ void hostImportReportTests() { counts.requesters[static_cast(ImportRequester::Map)] = {2, 1u << 20u, 0, 0, 0}; counts.requesters[static_cast(ImportRequester::Build)] = {5, 4u << 20u, 7, 7u << 19u, 1, 4}; counts.requesters[static_cast(ImportRequester::Segment)] = {1, 1u << 16u, 0, 0, 2}; + counts.requesters[static_cast(ImportRequester::ColorTarget)] = {1, 1u << 20u, 0, 0, 0}; const auto text = FormatHostImportCounts(counts); - Require(text == "3 live (3.0 MiB); made 8 (5.1 MiB), refused 7 (3.5 MiB, 4 remembered), failed 3; by requester (made/refused/failed): map 2/0/0, build 5/7/1, access 0/0/0, join 0/0/0, segment 1/0/2", "the host import report is wrong: " + text); + Require(text == "3 live (3.0 MiB); made 9 (6.1 MiB), refused 7 (3.5 MiB, 4 remembered), failed 3; by requester (made/refused/failed): map 2/0/0, build 5/7/1, access 0/0/0, join 0/0/0, segment 1/0/2, color target 1/0/0", "the host import report is wrong: " + text); } class PieceWindow { @@ -2360,6 +2380,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()) { @@ -4168,6 +4246,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); 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/FmaMixOnce.cpp b/core/libs/prx/libSceAgcDriver/tests/FmaMixOnce.cpp index aa48f4f1e7..c377b045d7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FmaMixOnce.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FmaMixOnce.cpp @@ -82,7 +82,7 @@ int main() { const auto one = Compile(1); const auto three = Compile(3); Check(three.words - one.words < one.words - none.words, "fma mix once: 2 more v_fma_mixlo/mixhi_f16 added " + std::to_string(three.words - one.words) + " SPIR-V words, as many as the first one with its shared emulation (" + std::to_string(one.words - none.words) + ")"); - Check(one.functions == none.functions + 1u, "fma mix once: one v_fma_mixlo_f16 emitted " + std::to_string(one.functions - none.functions) + " functions more than none, expected one shared emulation function"); + Check(one.functions == none.functions + 2u, "fma mix once: one v_fma_mixlo_f16 emitted " + std::to_string(one.functions - none.functions) + " functions more than none, expected one shared f32 path and its shared exact emulation"); Check(three.functions == one.functions, "fma mix once: two more v_fma_mixlo/mixhi_f16 in the same float mode emitted " + std::to_string(three.functions - one.functions) + " more functions, expected calls to the shared one"); std::printf("fma mix once tests passed (%zu words without v_fma_mix, %zu for 1, %zu for 3)\n", none.words, one.words, three.words); return 0; 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 c29910a617..e1896d291f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3973,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; @@ -4674,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; @@ -9169,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; @@ -9479,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) { @@ -9993,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(); @@ -10470,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"); } @@ -12294,6 +12485,7 @@ int main(int argc, char** argv) { srgb8TargetTests(); DepthClipTests(); DepthRemapTests(); + DepthBoundsEmulationTests(); UnusedViewportDepthTests(); DepthStencilTests(); OneDimensionalColorTests(); @@ -12304,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(); 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/HostImportRetireHold.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp index 8d0d9cddc7..384299be2c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp @@ -360,6 +360,23 @@ void BusyRetireOutlivesALaterBatch() { Expect(!Live(retired), "the dropped import outlived every batch of its hold"); } +void RecentImportFollowsReplacement() { + Heap heap; + ActiveRecorder active; + const auto context = mockContext(); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto* first = HostImportFor(context, heap.Address(), 4096); + Expect(first != nullptr, "the first range was not imported"); + if (first == nullptr) return; + Expect(HostImportFor(context, heap.Address(), 4096) == first, "a repeated lookup did not find the same import"); + const auto retired = first->buffer; + heap.Grow(); + const auto* grown = HostImportFor(context, heap.Address(RangeBytes), 4096); + Expect(grown != nullptr && grown->buffer != retired, "the grown run was not imported as a new import"); + if (grown == nullptr) return; + Expect(HostImportFor(context, heap.Address(), 4096) == grown, "a lookup after the replacement served the retired import instead of the one that replaced it"); +} + } int main() { @@ -374,6 +391,7 @@ int main() { IdleRetireOutlivesTheHold(); BusyRetireOutlivesALaterBatch(); AgcDriver::Tests::RunHostImportFailureTests(mockContext(), false); + RecentImportFollowsReplacement(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); return 1; 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/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp index 2c138fecf3..8a5ad4b746 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp @@ -2,7 +2,9 @@ #include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" #include "prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libc/include/General.hpp" #include #include #include @@ -18,6 +20,7 @@ #include #include #include +#include #include namespace { @@ -533,6 +536,206 @@ void fullPanelTest(const Device& device, PerformanceOverlay& overlay) { for (const auto& line : content.below) std::printf(" %s\n", line.c_str()); } +AgcDriver::OverlayDetailSample pageTwoSample() { + AgcDriver::OverlayDetailSample sample; + sample.fps = 59.94; + sample.frameMs = 16.7; + sample.busy = {{"Q0", 63}, {"Q20", 4}, {"COMMIT", 12}, {"FINISH", 3}, {"PRESENT", 6}}; + sample.phases = {{"Driver.Packet", 3.2}, {"Vulkan.GraphicsPipeline.pipeline_create", 1.5}, {"Recorder.Sync", 0.4}}; + sample.queueWaits = AgcDriver::OverlayQueueWaits{0.4, 0.2, 1.3}; + sample.renderPassesPerFrame = 12; + sample.pipelinesCompiling = 3; + sample.pipelinesPerSecond = 12; + sample.drawCacheHitPercent = 97.1; + sample.pipelineCacheHitPercent = 99.9; + sample.flipsPerSecond = 60; + sample.presentsPerSecond = 59.94; + sample.droppedFrames = 0; + sample.repeatedPresents = 0; + sample.commit = AgcDriver::OverlayMemory{12ull << 30u, 31ull << 30u}; + sample.pageFaultsPerSecond = 1234; + sample.hostImports = 123; + sample.hostImportBytes = AgcDriver::OverlayMemory{1ull << 30u, 6ull << 30u}; + sample.cpuCopyMegabytesPerSecond = 12.3; + sample.addressCopyCapPercent = 3; + sample.unimplementedPerSecond = 0; + sample.unimplementedTotal = 4; + sample.refusedShaders = 2; + sample.title = "PPSA01234"; + sample.presentMode = "fifo"; + return sample; +} + +void pageTwoTest(const Context& context, PerformanceOverlay& overlay) { + const VkExtent2D extent{1280, 720}; + VkClearColorValue clear{}; + clear.float32[0] = 0.25f; + clear.float32[1] = 0.5f; + clear.float32[2] = 0.75f; + clear.float32[3] = 1.0f; + const auto content = AgcDriver::ComposeOverlayDetails(pageTwoSample()); + OverlayRect bounds{}; + const auto reference = clearAndOverlay(context, nullptr, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopRight, nullptr); + const auto drawn = clearAndOverlay(context, &overlay, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopRight, &bounds); + const auto layout = AgcDriver::LayOutOverlay(content, extent, OverlayCorner::TopRight); + Require(bounds.x == layout.bounds.x && bounds.y == layout.bounds.y && bounds.width == layout.bounds.width && bounds.height == layout.bounds.height, "page 2 is not drawn where its layout puts it"); + onlyInside(reference, drawn, extent, bounds, "page 2 at 1280x720"); + std::int32_t row = OverlayMetrics::Padding; + for (const auto& line : content.below) { + requireTextDrawn(drawn, extent, bounds, layout.scale, reference.front(), line, row); + row += OverlayMetrics::LinePitch; + } + std::printf("page 2 at 1280x720: %dx%d at %d,%d (scale %d), every line drawn bit for bit, nothing changed outside it:\n", bounds.width, bounds.height, bounds.x, bounds.y, layout.scale); + for (const auto& line : content.below) std::printf(" |%-46s|\n", line.c_str()); +} + +void pageCycleTest(const Device& device) { + const auto& context = device.GetContext(); + const VkExtent2D extent{1280, 720}; + Require(PerformanceOverlay::Page() == 0 && !PerformanceOverlay::Visible(), "the performance overlay does not start hidden without ANYPS5_OVERLAY"); + PerformanceOverlay overlay(context, 1); + const auto start = AgcDriver::OverlayStatistics::Clock::now(); + const auto present = [&](int frame) { overlay.NotePresent(start + std::chrono::milliseconds(16) * frame, VkExtent2D{640, 360}); }; + const auto record = [&] { + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + OverlayRect bounds{}; + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + batch.SubmitAndWait(); + overlay.Release(0); + return bounds; + }; + PerformanceOverlay::Toggle(); + Require(PerformanceOverlay::Page() == 1 && PerformanceOverlay::Visible(), "the first F10 does not show page 1"); + for (int frame = 0; frame < 5; ++frame) present(frame); + Require(overlay.ShownPage() == 1 && overlay.Statistics().FrameTimes().size() == 4, "page 1 did not keep its frames"); + const auto pageOne = record(); + PerformanceOverlay::Toggle(); + Require(PerformanceOverlay::Page() == 2 && PerformanceOverlay::Visible(), "the second F10 does not show page 2"); + present(5); + Require(overlay.ShownPage() == 2 && overlay.Statistics().FrameTimes().empty(), "changing to page 2 kept page 1's statistics"); + for (int frame = 6; frame < 10; ++frame) present(frame); + const auto& sample = overlay.DetailSample(); + Require(std::abs(sample.frameMs - 16.0) < 1e-6 && sample.presentsPerSecond.has_value() && sample.pipelinesPerSecond.has_value(), "page 2 did not sample its frames"); + const auto pageTwo = record(); + const auto expected = AgcDriver::LayOutOverlay(AgcDriver::ComposeOverlayDetails(sample), extent, PerformanceOverlay::Corner()).bounds; + Require(pageTwo.width == expected.width && pageTwo.height == expected.height && pageTwo.width > pageOne.width, "page 2 is not drawn from its own composer: " + std::to_string(pageTwo.width) + "x" + std::to_string(pageTwo.height)); + PerformanceOverlay::Toggle(); + Require(PerformanceOverlay::Page() == 0 && !PerformanceOverlay::Visible(), "the third F10 does not hide the overlay"); + present(10); + Require(overlay.ShownPage() == 0 && overlay.Statistics().FrameTimes().empty() && overlay.DetailSample().fps == 0, "hiding the overlay kept page 2's state"); + std::printf("F10 cycle: off, page 1 (%dx%d), page 2 (%dx%d), off; each change of page resets the statistics\n", pageOne.width, pageOne.height, pageTwo.width, pageTwo.height); +} + +AgcDriver::OverlayDriverCounters fakeDriverCounters() { + static std::uint64_t calls = 0; + ++calls; + return AgcDriver::OverlayDriverCounters{calls * 66, AgcDriver::OverlayHits{calls * 1000, calls * 900}}; +} + +void pageTwoSamplerTest(const Device& device) { + using AgcDriver::PerformanceTrace; + using namespace std::chrono_literals; + const auto& context = device.GetContext(); + const VkExtent2D extent{1280, 720}; + Require(PerformanceOverlay::Page() == 0 && !PerformanceTrace::Recording(), "the trace records before page 2 is shown"); + PerformanceOverlay overlay(context, 1); + PerformanceOverlay::SetDriverCounters(&fakeDriverCounters); + PerformanceOverlay::Toggle(); + Require(!PerformanceTrace::Recording(), "page 1 turns the trace on"); + PerformanceOverlay::Toggle(); + Require(PerformanceOverlay::Page() == 2 && PerformanceTrace::Recording() && PerformanceTrace::OverlayTotals(), "page 2 does not turn the trace totals on"); + const auto start = AgcDriver::OverlayStatistics::Clock::now(); + const auto present = [&](std::chrono::milliseconds at, std::uint64_t address, std::uint64_t drops) { overlay.NotePresent(start + at, VkExtent2D{640, 360}, AgcDriver::OverlayPresentInfo{address, VK_PRESENT_MODE_FIFO_KHR, drops}); }; + present(0ms, 0x1000, 7); + const auto& sample = overlay.DetailSample(); + Require(sample.commit.has_value() && sample.unimplementedTotal.has_value() && sample.presentMode == "fifo" && sample.pipelinesCompiling == std::optional{0}, "page 2's first sample does not read the commit charge, the unimplemented calls, the compile backlog and the present mode"); + Require(!sample.flipsPerSecond && !sample.pageFaultsPerSecond && !sample.cpuCopyMegabytesPerSecond && sample.busy.empty() && sample.phases.empty(), "page 2's first sample shows rates before a second has passed"); + Require(sample.hostImports.has_value() == (context.hostImportAlignment != 0), "the host import line does not follow the device's host import support"); + std::thread([] { + PerformanceTrace::BindThread("queue worker 0x0"); + if (auto* trace = PerformanceTrace::Thread()) { + const auto begin = std::chrono::steady_clock::now(); + trace->Add("Driver.Packet", nullptr, begin, begin + 110ms); + } + for (int pass = 0; pass < 6; ++pass) PerformanceTrace::Count(PerformanceTrace::RenderPassScope); + AgcDriver::Graphics::AddressCopyCounters::ThisThread().Count(std::uint64_t{11} << 20u); + }).join(); + try { + NotImplemented_nid_no_patch("page 2 sampler test"); + } catch (const std::runtime_error&) { + } + present(16ms, 0x1000, 7); + present(33ms, 0x2000, 8); + present(1100ms, 0x2000, 9); + const auto near = [](const std::optional& value, double expected) { return value.has_value() && std::abs(*value - expected) < 1e-6; }; + Require(near(sample.flipsPerSecond, 60) && near(sample.drawCacheHitPercent, 90), "66 flips and 900 of 1000 draw-cache lookups over 1.1 s are not 60 flips/s and 90 %"); + Require(sample.droppedFrames == std::optional{2} && sample.repeatedPresents == std::optional{2}, "2 dropped and 2 repeated presents over 1.1 s are not shown as 2/s each"); + Require(sample.pageFaultsPerSecond.has_value() && near(sample.unimplementedPerSecond, 1 / 1.1), "the page faults or the unimplemented calls per second are missing"); + Require(near(sample.cpuCopyMegabytesPerSecond, 10), "11 MiB of address copies over 1.1 s are not 10 MB/s"); + if (std::getenv("APS5_ADDRESS_COPY_MAX_MIB") == nullptr) Require(near(sample.addressCopyCapPercent, 100.0 * 11 / 64), "an 11 MiB build is not 17.2 % of the 64 MiB cap"); + Require(near(sample.renderPassesPerFrame, 2), "6 render passes over 3 frames are not 2 per frame"); + const auto queue = std::find_if(sample.busy.begin(), sample.busy.end(), [](const AgcDriver::OverlayThreadBusy& busy) { return busy.name == "Q0"; }); + Require(queue != sample.busy.end() && std::abs(queue->percent - 10) < 1e-6 && sample.queueWaits.has_value(), "110 ms of packets over 1.1 s do not make queue 0 10 % busy"); + Require(!sample.phases.empty() && sample.phases.front().name == "Driver.Packet" && std::abs(sample.phases.front().msPerFrame - 110.0 / 3) < 1e-6, "the top phase is not 110 ms of packets over 3 frames"); + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + const auto bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + batch.SubmitAndWait(); + overlay.Release(0); + const auto content = AgcDriver::ComposeOverlayDetails(sample); + const auto expected = AgcDriver::LayOutOverlay(content, extent, PerformanceOverlay::Corner()).bounds; + Require(bounds.width == expected.width && bounds.height == expected.height, "page 2 is not drawn from its sample"); + std::printf("page 2 from its sampler (fake driver counters, a 'queue worker 0x0' thread, this process's OS counters):\n"); + for (const auto& line : content.below) std::printf(" |%-46s|\n", line.c_str()); + PerformanceOverlay::Toggle(); + Require(PerformanceOverlay::Page() == 0 && !PerformanceTrace::Recording() && !PerformanceTrace::OverlayTotals(), "hiding the overlay leaves the trace totals on"); + present(1200ms, 0x3000, 9); + Require(overlay.ShownPage() == 0 && overlay.DetailSample().fps == 0 && overlay.DetailSample().busy.empty() && !overlay.DetailSample().commit, "hiding the overlay kept page 2's sample"); + PerformanceOverlay::SetDriverCounters(nullptr); +} + +void presentCostReport(const Device& device) { + using AgcDriver::PerformanceCounters; + using AgcDriver::PerformanceTrace; + using Clock = AgcDriver::OverlayStatistics::Clock; + const auto& context = device.GetContext(); + const VkExtent2D extent{1920, 1080}; + Require(PerformanceOverlay::Page() == 0, "the cost report does not start hidden"); + PerformanceOverlay overlay(context, 1); + const auto start = Clock::now(); + const auto perCall = [](int calls, auto&& body) { + const auto began = std::chrono::steady_clock::now(); + for (int call = 0; call < calls; ++call) body(call); + return std::chrono::duration(std::chrono::steady_clock::now() - began).count() / calls; + }; + volatile bool shown = false; + const auto hiddenNs = perCall(10'000'000, [&](int) { shown = PerformanceOverlay::Visible(); }); + const auto passNs = perCall(10'000'000, [](int) { PerformanceTrace::Count(PerformanceTrace::RenderPassScope); }); + auto& copies = AgcDriver::Graphics::AddressCopyCounters::ThisThread(); + const auto copyNs = perCall(10'000'000, [&](int) { copies.Count(4096); }); + PerformanceOverlay::Toggle(); + const auto pageOneNs = perCall(20'000, [&](int frame) { + overlay.NotePresent(start + std::chrono::microseconds(16667) * frame, extent); + static_cast(AgcDriver::LayOutOverlay(AgcDriver::ComposeOverlay(overlay.Statistics(), PerformanceCounters::GpuEnabled(), PerformanceCounters::DriverEnabled(), overlay.Details()), extent, PerformanceOverlay::Corner())); + }); + PerformanceOverlay::Toggle(); + const auto later = start + std::chrono::hours(1); + const auto pageTwoNs = perCall(20'000, [&](int frame) { + overlay.NotePresent(later + std::chrono::microseconds(16667) * frame, extent, AgcDriver::OverlayPresentInfo{0x1000u + static_cast(frame % 2), VK_PRESENT_MODE_FIFO_KHR, 0}); + static_cast(AgcDriver::LayOutOverlay(AgcDriver::ComposeOverlayDetails(overlay.DetailSample()), extent, PerformanceOverlay::Corner())); + }); + const auto sampled = later + std::chrono::hours(1); + const auto sampleNs = perCall(200, [&](int second) { overlay.NotePresent(sampled + std::chrono::seconds(second), extent, AgcDriver::OverlayPresentInfo{0x1000, VK_PRESENT_MODE_FIFO_KHR, 0}); }); + PerformanceOverlay::Toggle(); + Require(!PerformanceOverlay::Visible() && !PerformanceTrace::Recording(), "the cost report does not end hidden"); + std::printf("present cost on this host: hidden %.1f ns per present (Visible), %.1f ns per render pass begun (the trace check), %.1f ns per address-copying build (the per-thread counter); page 1 %.2f us per present (NotePresent, compose, layout); page 2 %.2f us per present at 60 fps (NotePresent with its once-a-second sample, compose, layout), %.1f us per present that samples\n", hiddenNs, passNs, copyNs, pageOneNs / 1000, pageTwoNs / 1000, sampleNs / 1000); +} + void notePresentTest(const Device& device) { const auto& context = device.GetContext(); const VkExtent2D extent{1280, 720}; @@ -646,7 +849,10 @@ void disabledIsUnchangedTest(const Context& context) { onlyInside(drawnBefore, presenter.Drawn(&bounds), Presenter::Extent, bounds, "presentation pass with the overlay", false); onlyInside(blittedBefore, presenter.Blitted(&bounds), Presenter::Extent, bounds, "blit with the overlay", false); PerformanceOverlay::Toggle(); - Require(!PerformanceOverlay::Visible(), "F10 does not hide the performance overlay again"); + Require(PerformanceOverlay::Visible() && PerformanceOverlay::Page() == 2, "a second F10 does not show page 2"); + onlyInside(drawnBefore, presenter.Drawn(&bounds), Presenter::Extent, bounds, "presentation pass with page 2", false); + PerformanceOverlay::Toggle(); + Require(!PerformanceOverlay::Visible(), "a third F10 does not hide the performance overlay again"); Require(presenter.Drawn(&unused) == drawnBefore, "the presentation pass output differs after the overlay was hidden"); Require(presenter.Blitted(&unused) == blittedBefore, "the blit output differs after the overlay was hidden"); std::printf("overlay hidden: presentation pass and blit output bit-identical before and after showing it\n"); @@ -725,9 +931,13 @@ int main(int argc, char** argv) { hdrOverlayTest(context, overlay); cornerTest(context, overlay); fullPanelTest(*device, overlay); + pageTwoTest(context, overlay); costReport(*device, overlay); } notePresentTest(*device); + pageCycleTest(*device); + pageTwoSamplerTest(*device); + presentCostReport(*device); disabledIsUnchangedTest(context); std::cout << "performance overlay tests passed\n"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp index b829a08c01..316e631457 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp @@ -1,4 +1,8 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libc/include/General.hpp" #include #include #include @@ -10,6 +14,7 @@ #include #include #include +#include #include #include @@ -161,9 +166,10 @@ void lowRateTests() { void settingsTests() { const auto hidden = AgcDriver::ParseOverlaySettings(nullptr, nullptr); - require(!hidden.visible && hidden.corner == OverlayCorner::TopLeft, "the overlay is not off and top-left by default"); - require(!AgcDriver::ParseOverlaySettings("", "").visible && !AgcDriver::ParseOverlaySettings("0", nullptr).visible, "an empty or 0 ANYPS5_OVERLAY shows the overlay"); - require(AgcDriver::ParseOverlaySettings("1", nullptr).visible, "ANYPS5_OVERLAY=1 does not show the overlay"); + require(hidden.page == 0 && hidden.corner == OverlayCorner::TopLeft, "the overlay is not off and top-left by default"); + require(AgcDriver::ParseOverlaySettings("", "").page == 0 && AgcDriver::ParseOverlaySettings("0", nullptr).page == 0, "an empty or 0 ANYPS5_OVERLAY shows the overlay"); + require(AgcDriver::ParseOverlaySettings("1", nullptr).page == 1, "ANYPS5_OVERLAY=1 does not start on page 1"); + require(AgcDriver::ParseOverlaySettings("2", "br").page == 2 && AgcDriver::ParseOverlaySettings("2", "br").corner == OverlayCorner::BottomRight, "ANYPS5_OVERLAY=2 does not start on page 2 in the chosen corner"); const std::pair corners[] = {{"tl", OverlayCorner::TopLeft}, {"tr", OverlayCorner::TopRight}, {"bl", OverlayCorner::BottomLeft}, {"br", OverlayCorner::BottomRight}}; for (const auto& [name, corner] : corners) require(AgcDriver::ParseOverlaySettings("1", name).corner == corner, std::string("ANYPS5_OVERLAY_POS=") + name + " picks another corner"); const auto refused = [](const char* enabled, const char* position, const char* variable) { @@ -174,7 +180,12 @@ void settingsTests() { } return false; }; - require(refused("yes", nullptr, "ANYPS5_OVERLAY") && refused("2", nullptr, "ANYPS5_OVERLAY"), "an ANYPS5_OVERLAY value other than 1 or 0 was accepted or not named"); + require(refused("yes", nullptr, "ANYPS5_OVERLAY") && refused("3", nullptr, "ANYPS5_OVERLAY") && refused("02", nullptr, "ANYPS5_OVERLAY") && refused("-1", nullptr, "ANYPS5_OVERLAY"), "an ANYPS5_OVERLAY value other than 0, 1 or 2 was accepted or not named"); + try { + static_cast(AgcDriver::ParseOverlaySettings("3", nullptr)); + } catch (const std::exception& error) { + require(std::string(error.what()).find("0, 1 or 2") != std::string::npos, std::string("the ANYPS5_OVERLAY error does not list 0, 1 and 2: ") + error.what()); + } require(refused("1", "center", "ANYPS5_OVERLAY_POS") && refused(nullptr, "TL", "ANYPS5_OVERLAY_POS"), "an unknown ANYPS5_OVERLAY_POS was accepted or not named"); } @@ -431,6 +442,211 @@ void fullPanelLayoutTests() { } } +void fontTests() { + const auto characters = AgcDriver::OverlayFontCharacters(); + for (const char character : characters) { + const auto& glyph = AgcDriver::OverlayGlyph(character); + require(std::all_of(glyph.begin(), glyph.end(), [](std::uint8_t row) { return row <= 0x1f; }), std::string("the glyph of '") + character + "' is wider than 5 pixels"); + require(character == ' ' || std::any_of(glyph.begin(), glyph.end(), [](std::uint8_t row) { return row != 0; }), std::string("the glyph of '") + character + "' is empty"); + for (const char other : characters) { + if (other != character) require(glyph != AgcDriver::OverlayGlyph(other), std::string("'") + character + "' and '" + other + "' have the same glyph"); + } + } + for (const char letter : std::string("JQYZ%+-_")) require(characters.find(letter) != std::string_view::npos, std::string("the font lists no '") + letter + "'"); + for (const char missing : std::string("qjy(>,#")) { + bool refused = false; + try { + static_cast(AgcDriver::OverlayGlyph(missing)); + } catch (const std::exception&) { + refused = true; + } + require(refused && characters.find(missing) == std::string_view::npos, std::string("'") + missing + "' is drawn or listed although the font has no glyph for it"); + } +} + +void widePanelTests() { + OverlayContent wide; + wide.columns = 48; + wide.graph = false; + for (int i = 0; i < 20; ++i) wide.below.push_back(std::string(48, i % 2 == 0 ? 'W' : 'Z')); + const VkExtent2D small{1280, 720}; + const auto layout = AgcDriver::LayOutOverlay(wide, small, OverlayCorner::TopLeft); + const auto s = layout.scale; + require(layout.bounds.width == (48 * OverlayMetrics::Advance - 1 + 2 * OverlayMetrics::Padding) * s, "a 48-column panel is " + std::to_string(layout.bounds.width) + " pixels wide at scale " + std::to_string(s)); + require(layout.bounds.height == (2 * OverlayMetrics::Padding + 20 * OverlayMetrics::LinePitch - (OverlayMetrics::LinePitch - OverlayMetrics::GlyphHeight)) * s, "a panel without a graph keeps the graph's rows"); + const auto text = coverage(layout, static_cast(AgcDriver::OverlayColor::Text)); + for (std::size_t line = 0; line < wide.below.size(); ++line) requireLineDrawn(layout, text, wide.below[line], OverlayMetrics::Padding + static_cast(line) * OverlayMetrics::LinePitch); + for (const auto& rect : layout.rects) require(rect.color != static_cast(AgcDriver::OverlayColor::Graph), "a panel without a graph draws graph bars"); + const std::pair scales[] = {{{1280, 720}, 2}, {{1920, 1080}, 2}, {{3840, 2160}, 5}, {{320, 180}, 1}}; + for (const auto& [extent, scale] : scales) require(AgcDriver::OverlayScale(extent, wide) == scale, "a 48-column panel at " + std::to_string(extent.width) + "x" + std::to_string(extent.height) + " is drawn at scale " + std::to_string(AgcDriver::OverlayScale(extent, wide)) + ", not " + std::to_string(scale)); + auto widest = wide; + widest.columns = 140; + require(AgcDriver::OverlayScale({3840, 2160}, widest) == 4, "a 140-column panel at 3840x2160 is not shrunk from scale 5 to 4"); + const auto refused = [](const OverlayContent& content) { + try { + static_cast(AgcDriver::LayOutOverlay(content, {1920, 1080}, OverlayCorner::TopLeft)); + } catch (const std::exception&) { + return true; + } + return false; + }; + auto longer = wide; + longer.below.push_back(std::string(49, 'W')); + require(refused(longer), "a line longer than the panel's columns was drawn"); + auto graphless = wide; + graphless.frameTimes = {16.0}; + require(refused(graphless), "frame times were laid out on a panel without a graph"); + OverlayContent cramped{{"FPS: 60.0"}, {16.0}, {}}; + cramped.columns = 20; + require(refused(cramped), "a graph of 120 frames was laid out on a 20-column panel"); + cramped.columns = 21; + require(!refused(cramped), "a 21-column panel cannot hold the graph"); +} + +void pageCycleTests() { + for (std::uint32_t start = 0; start < AgcDriver::OverlayPageCount; ++start) { + for (std::uint64_t presses = 0; presses <= 3; ++presses) { + const auto page = AgcDriver::OverlayPageAfter(start, presses); + require(page == (start + presses) % 3, "starting on page " + std::to_string(start) + ", " + std::to_string(presses) + " presses of F10 show page " + std::to_string(page)); + } + } + require(AgcDriver::OverlayPageAfter(2, ~std::uint64_t{0}) == 2 && AgcDriver::OverlayPageAfter(1, ~std::uint64_t{0} - 1) == 0, "the page wraps wrongly after 2^64 presses"); + bool refused = false; + try { + static_cast(AgcDriver::OverlayPageAfter(3, 0)); + } catch (const std::exception&) { + refused = true; + } + require(refused, "a starting page 3 was accepted"); +} + +AgcDriver::OverlayDetailSample fullDetailSample() { + AgcDriver::OverlayDetailSample sample; + sample.fps = 59.94; + sample.frameMs = 16.7; + sample.busy = {{"Q0", 63.4}, {"Q20", 4.2}, {"COMMIT", 12}, {"FINISH", 3}, {"PRESENT", 5.5}, {"Q21", 100}, {"Q22", 0}}; + sample.phases = {{"Driver.Packet", 3.21}, {"Vulkan.GraphicsPipelineLibrary.shader_compile_and_link", 12.5}, {"Recorder.Sync", 0.4}, {"GpuMutex.wait.draw", 0.06}, {"Driver.Draw.capture_resources", 0.9}}; + sample.queueWaits = AgcDriver::OverlayQueueWaits{0.4, 0.2, 1.3}; + sample.renderPassesPerFrame = 12; + sample.pipelinesCompiling = 3; + sample.pipelinesPerSecond = 12; + sample.drawCacheHitPercent = 97.1; + sample.pipelineCacheHitPercent = 99.9; + sample.flipsPerSecond = 60; + sample.presentsPerSecond = 59.94; + sample.droppedFrames = 1; + sample.repeatedPresents = 2; + sample.commit = AgcDriver::OverlayMemory{static_cast(12.3 * GiB), static_cast(31.9 * GiB)}; + sample.pageFaultsPerSecond = 1234; + sample.hostImports = 123; + sample.hostImportBytes = AgcDriver::OverlayMemory{static_cast(1.2 * GiB), 6 * GiB}; + sample.cpuCopyMegabytesPerSecond = 12.3; + sample.addressCopyCapPercent = 3; + sample.unimplementedPerSecond = 0; + sample.unimplementedTotal = 4; + sample.refusedShaders = 2; + sample.title = "PPSA01234"; + sample.presentMode = "fifo_latest_ready"; + return sample; +} + +void requireDrawable(const OverlayContent& content, const std::string& what) { + const auto font = AgcDriver::OverlayFontCharacters(); + for (const auto* lines : {&content.above, &content.below}) { + for (const auto& line : *lines) { + require(line.size() <= static_cast(content.columns), what + ": '" + line + "' is wider than its " + std::to_string(content.columns) + " columns"); + for (const char character : line) { + require(font.find(character) != std::string_view::npos, what + ": '" + line + "' has '" + std::string(1, character) + "', which the font cannot draw"); + static_cast(AgcDriver::OverlayGlyph(character)); + } + } + } +} + +void pageTwoComposeTests() { + const auto full = AgcDriver::ComposeOverlayDetails(fullDetailSample()); + require(full.columns == OverlayMetrics::DetailColumns && !full.graph && full.above.empty() && full.frameTimes.empty(), "page 2 is not a 46-column panel without a graph"); + const auto phase = [](const std::string& name, const std::string& amount) { return name + std::string(46 - name.size() - amount.size(), ' ') + amount; }; + const std::vector expected{ + "FPS: 59.9 FRAME: 16.7 MS", + "BUSY %: Q0 63 Q20 4 COMMIT 12 FINISH 3", + " PRESENT 6 Q21 100 Q22 0", + phase("DRIVER.PACKET", " 3.2 MS/FRAME"), + "VULKAN.GRAPHICSPIPELINELIBRARY.S 12.5 MS/FRAME", + phase("RECORDER.SYNC", " 0.4 MS/FRAME"), + phase("GPUMUTEX.WAIT.DRAW", " 0.1 MS/FRAME"), + phase("DRIVER.DRAW.CAPTURE_RESOURCES", " 0.9 MS/FRAME"), + "Q0 WAIT MS/FRAME: MEM 0.4 LOCK 0.2 GPU 1.3", + "RENDER PASSES/FRAME: 12.0", + "PIPELINES: COMPILING 3 NEW/S 12", + "HIT %: DRAW CACHE 97.1 PIPELINES 99.9", + "FLIPS/S: 60.0 PRESENTS/S: 59.9", + "DROPPED/S: 1 REPEATED/S: 2", + "COMMIT: 12.3/31.9 GB FAULTS/S: 1234", + "HOST IMPORTS: 123 1.2/6.0 GB", + "CPU COPIES: 12.3 MB/S CAP: 3.0%", + "UNIMPLEMENTED: 0/S 4 TOTAL", + "REFUSED SHADERS: 2", + "TITLE: PPSA01234 PRESENT: FIFO_LATEST_READY", + }; + require(full.below.size() == expected.size(), "page 2 of a full sample has " + std::to_string(full.below.size()) + " lines, not " + std::to_string(expected.size())); + for (std::size_t line = 0; line < expected.size(); ++line) require(full.below[line] == expected[line], "page 2 line " + std::to_string(line) + " reads '" + full.below[line] + "', not '" + expected[line] + "'"); + requireDrawable(full, "page 2 of a full sample"); + + const auto empty = AgcDriver::ComposeOverlayDetails({}); + require(empty.below == std::vector{"FPS: 0.0 FRAME: 0.0 MS"}, "page 2 of an empty sample shows " + std::to_string(empty.below.size()) + " lines, the last '" + empty.below.back() + "'"); + + auto partial = fullDetailSample(); + partial.flipsPerSecond.reset(); + partial.commit.reset(); + partial.title.clear(); + partial.repeatedPresents.reset(); + partial.cpuCopyMegabytesPerSecond.reset(); + const auto some = AgcDriver::ComposeOverlayDetails(partial); + const auto has = [&](const std::string& line) { return std::find(some.below.begin(), some.below.end(), line) != some.below.end(); }; + require(has("PRESENTS/S: 59.9") && has("DROPPED/S: 1") && has("FAULTS/S: 1234") && has("PRESENT: FIFO_LATEST_READY") && has("CPU COPIES: CAP: 3.0%"), "a line with a missing source does not show the rest of its values alone"); + + AgcDriver::OverlayDetailSample odd; + odd.busy = {{"queue worker 0x57", 1e9}, {"q(1)", 1}}; + odd.phases = {{"Weird#with,symbols/and lower case and a very long tail beyond the panel", 1e9}}; + odd.title = "ab-cd_12"; + odd.presentMode = "mailbox"; + const auto sanitized = AgcDriver::ComposeOverlayDetails(odd); + requireDrawable(sanitized, "page 2 of names the font cannot draw"); + require(AgcDriver::OverlayText("abc-XYZ_09(%)é") == "ABC-XYZ_09 % ", "OverlayText maps '" + AgcDriver::OverlayText("abc-XYZ_09(%)é") + "'"); + for (const auto& line : sanitized.below) require(line.size() <= 46, "'" + line + "' is wider than page 2"); + + OverlayStatistics statistics = countedStatistics(); + auto details = fullDetails(); + details.display = {12345, 12345}; + details.window = {16384, 16384}; + requireDrawable(AgcDriver::ComposeOverlay(statistics, true, true, details), "page 1 of a full panel"); +} + +void pageTwoLayoutTests() { + const auto content = AgcDriver::ComposeOverlayDetails(fullDetailSample()); + const auto lines = static_cast(content.below.size()); + const auto width = 46 * OverlayMetrics::Advance - 1 + 2 * OverlayMetrics::Padding; + const auto height = 2 * OverlayMetrics::Padding + lines * OverlayMetrics::LinePitch - (OverlayMetrics::LinePitch - OverlayMetrics::GlyphHeight); + require(width + 2 * OverlayMetrics::Margin <= 1280 && height + 2 * OverlayMetrics::Margin <= 720, "page 2 does not fit 1280x720 at scale 1"); + const std::pair scales[] = {{{1280, 720}, 2}, {{1920, 1080}, 2}, {{3840, 2160}, 5}, {{640, 360}, 1}}; + for (const auto& [extent, scale] : scales) { + for (const auto corner : {OverlayCorner::TopLeft, OverlayCorner::BottomRight}) { + const auto layout = AgcDriver::LayOutOverlay(content, extent, corner); + require(layout.scale == scale, "page 2 at " + std::to_string(extent.width) + "x" + std::to_string(extent.height) + " is drawn at scale " + std::to_string(layout.scale) + ", not " + std::to_string(scale)); + require(layout.bounds.width == width * scale && layout.bounds.height == height * scale, "page 2's panel is " + std::to_string(layout.bounds.width) + "x" + std::to_string(layout.bounds.height) + " at scale " + std::to_string(scale)); + require(layout.bounds.x >= 0 && layout.bounds.y >= 0 && layout.bounds.x + layout.bounds.width <= static_cast(extent.width) && layout.bounds.y + layout.bounds.height <= static_cast(extent.height), "page 2 leaves " + std::to_string(extent.width) + "x" + std::to_string(extent.height)); + for (const auto& rect : layout.rects) require(rect.color != static_cast(AgcDriver::OverlayColor::Graph), "page 2 draws graph bars"); + if (corner != OverlayCorner::TopLeft) continue; + const auto text = coverage(layout, static_cast(AgcDriver::OverlayColor::Text)); + for (std::size_t line = 0; line < content.below.size(); ++line) requireLineDrawn(layout, text, content.below[line], OverlayMetrics::Padding + static_cast(line) * OverlayMetrics::LinePitch); + } + } + auto stretched = content; + while (stretched.below.size() < 60) stretched.below.push_back("PIPELINES: COMPILING 3 NEW/S 12"); + require(AgcDriver::OverlayScale({3840, 2160}, stretched) == 3, "a 60-line page 2 at 3840x2160 is not shrunk to scale 3"); +} + void sampleTests() { require(AgcDriver::OverlaySampleDue(std::nullopt, Start), "the first frame is not sampled"); require(!AgcDriver::OverlaySampleDue(Start, Start + 999ms) && AgcDriver::OverlaySampleDue(Start, Start + 1000ms), "memory is sampled more than once a second, or not after a second"); @@ -446,6 +662,187 @@ void sampleTests() { require(touched[Bytes - 1] == 0x5a, "the touched block was not written"); } +constexpr const char* PacketScope = "Driver.Packet"; +constexpr const char* WorkerWaitScope = "Driver.WorkerWait"; +constexpr const char* LockScope = "GpuMutex.wait"; +constexpr const char* SyncScope = "Recorder.Sync"; +constexpr const char* SerialScope = "Recorder.WaitSerial"; +constexpr const char* WaitScope = "Graphics.Wait"; +constexpr const char* FinishScope = "Driver.DrawFinish"; +constexpr const char* MemoryStage = "memory_wait"; +constexpr const char* RenderingStage = "rendering_wait"; +constexpr const char* DrawSite = "draw"; +constexpr const char* PresentSite = "present"; +constexpr const char* FenceStage = "fence_wait"; + +AgcDriver::TraceThread::Total total(const char* scope, const char* stage, std::chrono::milliseconds length, std::uint64_t count) { + return AgcDriver::TraceThread::Total{scope, stage, std::chrono::duration_cast(length), count}; +} + +void traceDeltaTests() { + using AgcDriver::TraceThreadTotals; + const std::vector before{ + {"queue worker 0x0", {total(PacketScope, nullptr, 100ms, 10), total(PacketScope, MemoryStage, 5ms, 1), total(WorkerWaitScope, nullptr, 50ms, 2)}}, + {"queue worker 0x1", {total(PacketScope, nullptr, 10ms, 1)}}, + }; + const std::vector now{ + {"queue worker 0x0", {total(PacketScope, nullptr, 500ms, 60), total(PacketScope, MemoryStage, 35ms, 3), total(PacketScope, RenderingStage, 10ms, 1), total(LockScope, DrawSite, 20ms, 4), total(LockScope, PresentSite, 5ms, 1), total(SyncScope, nullptr, 6ms, 2), total(SerialScope, nullptr, 4ms, 1), total(WaitScope, FenceStage, 5ms, 1), total(WaitScope, nullptr, 9ms, 1), total(WorkerWaitScope, nullptr, 450ms, 40)}}, + {"queue worker 0x1", {total(PacketScope, nullptr, 110ms, 2)}}, + {"queue worker 0xa", {total(PacketScope, nullptr, 50ms, 1)}}, + {"draw committer", {total(AgcDriver::PerformanceTrace::RenderPassScope, nullptr, 0ms, 120), total(FinishScope, nullptr, 80ms, 40), total(SyncScope, nullptr, 7ms, 1)}}, + }; + const auto figures = AgcDriver::OverlayTraceDeltas(before, now, 1s, 40, true); + require(figures.busy.size() == 3 && figures.busy[0].name == "Q0" && figures.busy[1].name == "Q1" && figures.busy[2].name == "Qa", "the busy queues are not Q0, Q1 and Qa in that order"); + require(within(figures.busy[0].percent, 40) && within(figures.busy[1].percent, 10) && within(figures.busy[2].percent, 5), "the busy shares over a second are not 40, 10 and 5 %"); + const std::vector> phases{{"Driver.Packet", 13.75}, {"Driver.DrawFinish", 2}, {"Driver.Packet.memory_wait", 0.75}, {"GpuMutex.wait.draw", 0.5}, {"Recorder.Sync", 0.325}}; + require(figures.phases.size() == phases.size(), "page 2 does not keep the top 5 phases"); + for (std::size_t index = 0; index < phases.size(); ++index) require(figures.phases[index].name == phases[index].first && within(figures.phases[index].msPerFrame, phases[index].second), "phase " + std::to_string(index) + " is " + figures.phases[index].name + " at " + std::to_string(figures.phases[index].msPerFrame) + " ms per frame, not " + phases[index].first); + require(figures.queueWaits && within(figures.queueWaits->memoryMs, 1.0) && within(figures.queueWaits->lockMs, 0.625) && within(figures.queueWaits->gpuMs, 0.375), "queue 0's waits per frame are not 1 ms on memory, 0.625 ms on the lock and 0.375 ms on the GPU"); + require(figures.renderPassesPerFrame && within(*figures.renderPassesPerFrame, 3), "120 render passes over 40 frames are not 3 per frame"); + require(!AgcDriver::OverlayTraceDeltas(before, now, 1s, 40, false).renderPassesPerFrame, "render passes are shown while they are not counted"); + const auto noFrames = AgcDriver::OverlayTraceDeltas(before, now, 1s, 0, true); + require(noFrames.busy.size() == 3 && noFrames.phases.empty() && !noFrames.queueWaits && !noFrames.renderPassesPerFrame, "per-frame figures are shown for a second without frames"); + require(AgcDriver::OverlayTraceDeltas(before, now, Clock::duration{}, 40, true).busy.empty(), "busy shares are shown for no elapsed time"); + const auto noQueue = AgcDriver::OverlayTraceDeltas({}, std::vector{now[3]}, 1s, 40, true); + require(noQueue.busy.empty() && !noQueue.queueWaits && noQueue.renderPassesPerFrame, "a trace without queue workers shows busy shares or queue waits"); + AgcDriver::OverlayDetailSample sample; + sample.busy = figures.busy; + sample.phases = figures.phases; + sample.queueWaits = figures.queueWaits; + sample.renderPassesPerFrame = figures.renderPassesPerFrame; + requireDrawable(AgcDriver::ComposeOverlayDetails(sample), "page 2 from trace totals"); +} + +void hitRateTests() { + require(within(AgcDriver::OverlayHitPercent({100, 80}, {300, 230}).value_or(-1), 75), "150 hits of 200 lookups are not 75 %"); + require(within(AgcDriver::OverlayHitPercent({1000, 900}, {200, 150}).value_or(-1), 75), "counters that restarted (the draw cache's 10 s report) are not read from zero"); + require(!AgcDriver::OverlayHitPercent({5, 5}, {5, 5}), "a hit rate is shown without lookups"); + require(!AgcDriver::OverlayHitPercent({0, 0}, {4, 5}), "more hits than lookups give a hit rate"); +} + +void presentModeTests() { + require(AgcDriver::OverlayPresentModeName(VK_PRESENT_MODE_FIFO_KHR) == "fifo" && AgcDriver::OverlayPresentModeName(VK_PRESENT_MODE_FIFO_LATEST_READY_KHR) == "fifo_latest_ready" && AgcDriver::OverlayPresentModeName(VK_PRESENT_MODE_MAILBOX_KHR) == "mailbox" && AgcDriver::OverlayPresentModeName(VK_PRESENT_MODE_IMMEDIATE_KHR) == "immediate", "the present modes are not named as ANYPS5_PRESENT_MODE names them"); + require(AgcDriver::OverlayPresentModeName(VK_PRESENT_MODE_FIFO_RELAXED_KHR).empty() && AgcDriver::OverlayPresentModeName(VK_PRESENT_MODE_MAX_ENUM_KHR).empty(), "a present mode the driver never selects has a name"); + AgcDriver::OverlayDetailSample sample; + sample.title = "PPSA01234"; + sample.presentMode = AgcDriver::OverlayPresentModeName(VK_PRESENT_MODE_FIFO_LATEST_READY_KHR); + const auto content = AgcDriver::ComposeOverlayDetails(sample); + require(content.below.back() == "TITLE: PPSA01234 PRESENT: FIFO_LATEST_READY", "the longest present mode is drawn as '" + content.below.back() + "'"); +} + +void operatingSystemTests() { + const auto parsed = AgcDriver::ParseCommitCharge("MemTotal: 16318412 kB\nCommitLimit: 8159204 kB\nCommitted_AS: 12345678 kB\nVmallocTotal: 34359738367 kB\n"); + require(parsed && parsed->used == 12345678ull * 1024 && parsed->budget == 8159204ull * 1024, "Committed_AS and CommitLimit are not read from /proc/meminfo's text"); + require(!AgcDriver::ParseCommitCharge("Committed_AS: 12345678 kB\n"), "a commit charge is shown without its limit"); + require(!AgcDriver::ParseCommitCharge("CommitLimit: 8159204 kB\nCommitted_AS: 12 MB\n"), "a value in another unit is read as kB"); + require(!AgcDriver::ParseCommitCharge(""), "an empty /proc/meminfo gives a commit charge"); + const auto commit = AgcDriver::ReadCommitCharge(); + require(commit && commit->used != 0 && commit->budget != 0, "the system's commit charge and limit cannot be read"); + static std::uint8_t* volatile touched = nullptr; + const auto before = AgcDriver::ReadPageFaults(); + require(before.has_value(), "the process's page faults cannot be read"); + constexpr std::size_t Bytes = std::size_t{16} << 20u; + const auto block = std::make_unique(Bytes); + touched = block.get(); + std::memset(touched, 0x3c, Bytes); + const auto after = AgcDriver::ReadPageFaults(); + require(after && *after > *before, "touching 16 MiB of new memory did not raise the page faults from " + std::to_string(*before)); + require(touched[Bytes - 1] == 0x3c, "the touched block was not written"); +} + +void addressCopyFoldTests() { + using AgcDriver::Graphics::AddressCopyCounters; + const auto start = AddressCopyCounters::Fold(); + auto& counters = AddressCopyCounters::ThisThread(); + require(&counters == &AddressCopyCounters::ThisThread(), "one thread got two address copy counters"); + counters.Count(std::uint64_t{3} << 20u); + counters.Count(std::uint64_t{1} << 20u); + bool separate = false; + std::thread worker([&] { + auto& own = AddressCopyCounters::ThisThread(); + separate = &own != &counters; + own.Count(std::uint64_t{5} << 20u); + }); + worker.join(); + require(separate, "two builders share address copy counters"); + const auto folded = AddressCopyCounters::Fold(); + require(folded.bytes - start.bytes == std::uint64_t{9} << 20u && folded.largest == std::uint64_t{5} << 20u, "two builders' 9 MiB of copies with a largest build of 5 MiB fold to " + std::to_string(folded.bytes - start.bytes) + " bytes, largest " + std::to_string(folded.largest)); + const auto idle = AddressCopyCounters::Fold(); + require(idle.bytes == folded.bytes && idle.largest == 0, "a second without copies keeps the previous second's largest build"); + std::thread next([] { AddressCopyCounters::ThisThread().Count(std::uint64_t{2} << 20u); }); + next.join(); + const auto reused = AddressCopyCounters::Fold(); + require(reused.bytes == idle.bytes + (std::uint64_t{2} << 20u) && reused.largest == std::uint64_t{2} << 20u, "the copies of a builder that ended were lost, or a new builder restarted them"); + if (std::getenv("APS5_ADDRESS_COPY_MAX_MIB") == nullptr) require(AddressCopyCounters::Limit() == std::uint64_t{64} << 20u, "the address copy cap is not 64 MiB by default"); + AgcDriver::Graphics::SetAddressCopyLimit(std::uint64_t{8} << 20u); + require(AddressCopyCounters::Limit() == std::uint64_t{8} << 20u, "the cap in use is not the one set"); + AgcDriver::Graphics::SetAddressCopyLimit(0); +} + +void driverSourceTests() { + const auto before = NotImplementedCalls_nid_no_patch(); + try { + NotImplemented_nid_no_patch("performance overlay test"); + } catch (const std::runtime_error&) { + } + require(NotImplementedCalls_nid_no_patch() == before + 1, "an unimplemented-function call is not counted"); + if (std::getenv("APS5_SKIP_REFUSED_DRAWS") == nullptr) require(!AgcDriver::DriverDetail::RefusedShaderCount(), "refused shaders are counted while a refused shader still stops the title"); + require(AgcDriver::DriverDetail::RegistrationPreparationsPending() == std::optional{0}, "a process that prepared no shaders has a compile backlog"); +} + +void traceSwitchTests() { + using AgcDriver::PerformanceOverlay; + using AgcDriver::PerformanceTrace; + require(!PerformanceTrace::Configured(), "the overlay statistics test runs with APS5_TRACE_* set"); + require(PerformanceOverlay::Page() == 0 && !PerformanceTrace::Recording() && !PerformanceTrace::OverlayTotals(), "the trace records before page 2 is shown"); + std::string failure; + std::thread([&] { + if (PerformanceTrace::Thread() != nullptr) failure = "a thread is bound to the trace while nothing records"; + }).join(); + require(failure.empty(), failure); + PerformanceOverlay::Toggle(); + require(PerformanceOverlay::Page() == 1 && !PerformanceTrace::Recording() && !PerformanceTrace::OverlayTotals(), "page 1 turns the trace on"); + PerformanceOverlay::Toggle(); + require(PerformanceOverlay::Page() == 2 && PerformanceTrace::Recording() && PerformanceTrace::OverlayTotals(), "page 2 does not turn the trace totals on"); + std::thread([&] { + PerformanceTrace::BindThread("queue worker 0x5"); + auto* thread = PerformanceTrace::Thread(); + if (thread == nullptr || thread != PerformanceTrace::Thread()) { + failure = "a named worker is not bound once while page 2 shows"; + return; + } + const auto begin = Clock::now(); + thread->Add(PacketScope, nullptr, begin, begin + 3ms); + PerformanceTrace::Count(AgcDriver::PerformanceTrace::RenderPassScope); + PerformanceTrace::Count(AgcDriver::PerformanceTrace::RenderPassScope); + const auto snapshot = thread->Collect(); + if (!snapshot.slices.empty() || snapshot.dropped) failure = "page 2's trace totals buffer slices"; + }).join(); + require(failure.empty(), failure); + std::thread([&] { + if (PerformanceTrace::Thread() != nullptr) failure = "a thread that never named itself is bound while page 2 shows"; + }).join(); + require(failure.empty(), failure); + const auto threads = PerformanceTrace::ThreadTotals(); + require(threads.has_value(), "the trace totals cannot be read"); + const auto worker = std::find_if(threads->begin(), threads->end(), [](const AgcDriver::TraceThreadTotals& thread) { return thread.name == "queue worker 0x5"; }); + require(worker != threads->end(), "the named worker's totals are missing"); + std::uint64_t passes = 0; + Clock::duration packets{}; + for (const auto& entry : worker->totals) { + if (std::string_view(entry.scope) == AgcDriver::PerformanceTrace::RenderPassScope) passes += entry.count; + if (std::string_view(entry.scope) == PacketScope && entry.stage == nullptr) packets += entry.length; + } + require(passes == 2 && packets == 3ms, "the worker's totals hold " + std::to_string(passes) + " render passes and " + std::to_string(std::chrono::duration(packets).count()) + " ms of packets, not 2 and 3 ms"); + PerformanceOverlay::Toggle(); + require(PerformanceOverlay::Page() == 0 && !PerformanceTrace::Recording() && !PerformanceTrace::OverlayTotals(), "leaving page 2 leaves the trace totals on"); + std::thread([&] { + PerformanceTrace::Count(AgcDriver::PerformanceTrace::RenderPassScope); + if (PerformanceTrace::Thread() != nullptr) failure = "a thread is bound to the trace after page 2 was left"; + }).join(); + require(failure.empty(), failure); +} + void costReport() { auto& counters = AgcDriver::PacketCounters::ThisThread(); constexpr int Increments = 20'000'000; @@ -489,9 +886,21 @@ int main() { countFormatTests(); detailComposeTests(); fullPanelLayoutTests(); + fontTests(); + widePanelTests(); + pageCycleTests(); + pageTwoComposeTests(); + pageTwoLayoutTests(); sampleTests(); + traceDeltaTests(); + hitRateTests(); + presentModeTests(); + operatingSystemTests(); + addressCopyFoldTests(); + driverSourceTests(); + traceSwitchTests(); costReport(); - std::cout << "performance overlay statistics, layout, palette, packet, pipeline, memory and resolution tests passed\n"; + std::cout << "performance overlay statistics, layout, palette, packet, pipeline, memory, resolution, page 2 source and trace switch tests passed\n"; return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; 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 7a8cc7bd50..4a400c6f7d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -25,9 +25,11 @@ #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp" +#include "prx/libc/include/Api.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" +#include "prx/libc/include/Shutdown.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "ResidentPresent.hpp" #include "ImageTableAbi.hpp" @@ -66,6 +68,7 @@ #include #include #include +#include #include #include #include @@ -3025,15 +3028,33 @@ void requireImportServesGuest(const Context& context, Recorder& recorder, const const auto bytes = static_cast(end - begin); Require(begin >= import.base && end <= import.base + import.bytes, what + " is not inside its import"); const auto collected = CollectWritesUncached(begin, bytes); - Require(collected != 0 && Watched(begin, bytes), what + " left the write watch"); - Require(UnchangedSinceCollected(begin, page, collected) && UnchangedSinceCollected(end - page, page, collected), what + " reports writes nobody made"); + const bool watched = Watched(begin, bytes); + Require(!watched || collected != 0, what + " is watched but cannot collect writes"); + if (watched) { + Require(UnchangedSinceCollected(begin, page, collected) && UnchangedSinceCollected(end - page, page, collected), what + " reports writes nobody made"); + } else { + Require(collected == 0, what + " collects writes without being watched"); + const auto generation = WriteGeneration(); + Require(UntrackedHint(begin, bytes) == 0 && !UnchangedSince(begin, bytes, generation), what + " provides an unchanged or untracked-hint proof without a write watch"); + std::vector compared(bytes / page); + auto count = compared.size(); + GuestArena::GuestArenaComparedPages_nid_no_patch(begin, bytes, compared.data(), &count); + Require(count == compared.size(), what + " left the write watch without compared shared pages"); + for (std::size_t index = 0; index < count; ++index) { + Require(compared[index] == reinterpret_cast(begin + index * page), what + " has an unexpected compared shared page"); + MEMORY_BASIC_INFORMATION memory{}; + Require(VirtualQuery(compared[index], &memory, sizeof(memory)) == sizeof(memory) && memory.State == MEM_COMMIT, what + " has an inaccessible compared shared page"); + const auto protection = memory.Protect & 0xff; + Require(protection == PAGE_READWRITE || protection == PAGE_EXECUTE_READWRITE, what + " has a compared shared page that is not writable"); + } + } auto* first = reinterpret_cast(begin); auto* last = reinterpret_cast(end - page); for (std::size_t index = 0; index < probe; ++index) { first[index] = static_cast((index * 7u + salt) & 0xffu); last[index] = static_cast((index * 13u + salt * 5u + 1u) & 0xffu); } - Require(!UnchangedSinceCollected(begin, page, collected) && !UnchangedSinceCollected(end - page, page, collected), "a CPU store into " + what + " was not collected"); + if (watched) Require(!UnchangedSinceCollected(begin, page, collected) && !UnchangedSinceCollected(end - page, page, collected), "a CPU store into " + what + " was not collected"); Buffer readback(context, 2 * probe, VK_BUFFER_USAGE_TRANSFER_DST_BIT); const auto commands = recorder.Commands(); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); @@ -3163,7 +3184,7 @@ int reimportBench(const Device& device) { const auto* tail = HostImportFor(context, begin, static_cast(end - begin)); Require(tail != nullptr && tail->base == begin && tail->base + tail->bytes == end, "the re-import bench lost the import of its kept side"); requireImportServesGuest(context, recorder, *tail, begin, end, ++salt, std::string("the kept side after the ") + pattern.name + " cuts"); - std::printf("re-import bench, %s cuts: the kept side was imported again after %zu of %zu cuts, slowest import after a cut %.2f ms; CPU stores into it are collected and the GPU reads them through its buffer\n", pattern.name, again, moves, slowest); + std::printf("re-import bench, %s cuts: the kept side was imported again after %zu of %zu cuts, slowest import after a cut %.2f ms; write tracking sees its CPU stores and the GPU reads them through its buffer\n", pattern.name, again, moves, slowest); cleanup(); gpu.unlock(); return again; @@ -3284,7 +3305,7 @@ int hostImportCutTests(const Device& device) { } cleanup(); gpu.unlock(); - std::cout << "host import after cut: " << name << ": the changed import was retired before the remaining range was imported, and the GPU and write watch see the guest's bytes" << (test.unmap ? ", also after the pages were mapped again" : "") << '\n'; + std::cout << "host import after cut: " << name << ": the changed import was retired before the remaining range was imported, and the GPU and write tracking see the guest's bytes" << (test.unmap ? ", also after the pages were mapped again" : "") << '\n'; }; const auto remapWhileTrimmed = [&] { const std::string name = "an unmap and a new mapping of the retained side while the old import is retired"; @@ -3315,7 +3336,7 @@ int hostImportCutTests(const Device& device) { Require(remapped->memoryBase == begin && remapped->pinnedBytes == end - begin, "after " + name + " the pages mapped again are served from the import that pinned the old ones"); cleanup(); gpu.unlock(); - std::cout << "host import cut: " << name << ": the pages mapped again were imported again and the GPU and the write watch see the guest's bytes\n"; + std::cout << "host import cut: " << name << ": the pages mapped again were imported again and the GPU and write tracking see the guest's bytes\n"; }; gpu.unlock(); try { @@ -9609,6 +9630,29 @@ void minLodTests(const Device& device, Recorder& recorder) { expectRed(sample(0x100, 0.0f, 1), unorm(1), "minimum LOD clamp: MIN_LOD at the view's base level reads its base level"); } +void failedCheckReportTests(const Device& device, Recorder& recorder) { + const auto& context = device.GetContext(); + Require(Recorder::Active() == &recorder, "the failed check test needs the active recorder"); + TextureDetiler detiler(context); + GuestTextureResource resource{}; + resource.baseAddress = 0x100000; + resource.width = 64; + resource.height = 64; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kLinear; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const std::vector snapshot(static_cast(DescribeSurface(resource).guestBytes), std::byte{0x40}); + const VkComponentMapping identity{VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; + Texture texture(context, detiler, resource, identity, snapshot); + Require(recorder.Recording(), "a texture upload was not recorded into the open batch"); + Require(false, "a failed check stopped the test with a texture upload in its open batch"); +} + void singleCubeTests(const Device& device, Recorder& recorder) { auto context = device.GetContext(); TextureDetiler detiler(context); @@ -11058,6 +11102,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(); @@ -11325,7 +11460,23 @@ void Run(Context context) { #endif int main(int argc, char** argv) { - try { + _ZSt13set_terminatePFvvE_nid_postfix([] { + std::fflush(nullptr); + std::cout.flush(); + if (const auto failure = std::current_exception()) { + try { + std::rethrow_exception(failure); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + } catch (...) { + std::cerr << "terminated by an exception that is not a std::exception\n"; + } + } else { + std::cerr << "terminated without an active exception\n"; + } + LibcTerminate_nid_no_patch(1); + }); + { if (argc == 2 && std::string_view(argv[1]) == "--collect-mapping-history-only") { if (!collectMappingHistoryTests()) return 77; std::cout << "collect mapping history tests passed\n"; @@ -11438,6 +11589,10 @@ int main(int argc, char** argv) { } return sparseRunImportTests(device, true) ? 0 : 77; } + if (argc == 2 && std::string_view(argv[1]) == "--pipeline-library-lto-info-only") { + pipelineLibraryLtoInfoTests(device); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--pipeline-library-only") { pipelineLibraryTests(device); return 0; @@ -11508,6 +11663,10 @@ int main(int argc, char** argv) { std::cout << "CB resolves into tiled targets tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--failed-check-only") { + failedCheckReportTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--cube-only") { singleCubeTests(device, recorder); cubeExtraSliceTests(device, recorder); @@ -11845,8 +12004,5 @@ int main(int argc, char** argv) { recorder.Activate(); std::cout << "Recorder read tracking and label tests passed\n"; return 0; - } catch (const std::exception& error) { - std::cerr << error.what() << '\n'; - return 1; } } diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 6ee1fdace0..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 @@ -436,6 +437,90 @@ void verifySpecializedModuleRelease() { 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); @@ -8485,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"; diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index d2e874a808..fa67366d65 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -952,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(); @@ -962,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/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/UnsupportedDiagnostics.cpp b/core/libs/prx/libSceAgcDriver/tests/UnsupportedDiagnostics.cpp index 3217dc0eb1..85a226d1bf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/UnsupportedDiagnostics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/UnsupportedDiagnostics.cpp @@ -4,5 +4,7 @@ int main() { if (sceAgcDriverGetCoredumpAddress() != nullptr) return 1; if (sceAgcDriverSetTargetRingForDiag() != static_cast(0x8A6D0001)) return 2; + auto* volatile linked = &sceAgcDriverSetFlip; + if (linked == nullptr) return 3; return 0; } diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 336333e49c..1b542e6946 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -34,6 +34,7 @@ using namespace AgcDriver::GuestMemory; constexpr std::size_t Block = 65536; #ifdef _WIN32 +constexpr wchar_t MetadataCommitMode[] = L"--metadata-commit"; constexpr wchar_t HostHoleMode[] = L"--host-hole"; constexpr wchar_t ResidentRemapMode[] = L"--resident-direct-remap"; constexpr wchar_t RewatchImportPagesMode[] = L"--rewatch-import-pages"; @@ -62,7 +63,9 @@ bool CheckMetadataCommit() { Require(committed < (64u << 20u), "write tracking committed dense generation tables for untouched guest ranges"); const auto generation = TrackerGeneration(); for (std::uint64_t offset = 0; offset + Block <= bytes; offset += std::uint64_t{1} << 32u) { - Require(NewestStamp(arena + offset, Block) == 0 && UnchangedSince(arena + offset, Block, generation), "an untouched leaf has a write stamp"); + const bool host = GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(arena + offset, Block); + Require(NewestStamp(arena + offset, Block) == 0, "an untouched leaf has a write stamp"); + Require(UnchangedSince(arena + offset, Block, generation) != host, host ? "an arena host region reads as unchanged" : "an untouched leaf reads as changed"); } PROCESS_MEMORY_COUNTERS_EX queried{}; Require(K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&queried), sizeof(queried)), "cannot measure private commit after empty-range queries"); @@ -1255,11 +1258,18 @@ std::uint64_t ChildHostHole() { return (arena + arenaBytes / 2) & ~(std::uint64_t{Block} - 1); } -void CheckHostHoleInChild() { +std::uint64_t ProbedHostHole() { + std::uintptr_t arena = 0; + std::size_t arenaBytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arena, &arenaBytes); + return arena + ((arenaBytes / 2) & ~((std::uint64_t{1} << 32u) - 1)); +} + +void CheckHostHoleInChild(const wchar_t* mode, std::uint64_t hole) { std::array executable{}; const DWORD length = GetModuleFileNameW(nullptr, executable.data(), static_cast(executable.size())); Require(length != 0 && length < executable.size(), "cannot name the test executable"); - const std::wstring command = L"\"" + std::wstring(executable.data(), length) + L"\" " + HostHoleMode; + const std::wstring command = L"\"" + std::wstring(executable.data(), length) + L"\" " + mode; std::vector commandLine(command.begin(), command.end()); commandLine.push_back(L'\0'); STARTUPINFOW startup{}; @@ -1270,7 +1280,7 @@ void CheckHostHoleInChild() { startup.hStdError = GetStdHandle(STD_ERROR_HANDLE); PROCESS_INFORMATION process{}; Require(CreateProcessW(executable.data(), commandLine.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW | CREATE_SUSPENDED, nullptr, nullptr, &startup, &process) != 0, "cannot create the host-hole child"); - const bool reserved = VirtualAllocEx(process.hProcess, reinterpret_cast(ChildHostHole()), Block, MEM_RESERVE, PAGE_NOACCESS) != nullptr; + const bool reserved = VirtualAllocEx(process.hProcess, reinterpret_cast(hole), Block, MEM_RESERVE, PAGE_NOACCESS) != nullptr; const bool resumed = reserved && ResumeThread(process.hThread) != static_cast(-1); const bool exited = resumed && WaitForSingleObject(process.hProcess, 60000) == WAIT_OBJECT_0; if (!exited) { @@ -1718,6 +1728,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; @@ -1881,7 +1965,7 @@ void CheckAdoptLatestEpoch() { int main() { try { #ifdef _WIN32 - if (std::wstring_view(GetCommandLineW()).ends_with(L"--metadata-commit")) { + if (std::wstring_view(GetCommandLineW()).ends_with(MetadataCommitMode)) { return CheckMetadataCommit() ? 0 : 77; } #endif @@ -1936,7 +2020,8 @@ int main() { CheckPrivateMappingReuse(); CheckRewatch(); CheckUntrackedHint(); - CheckHostHoleInChild(); + CheckHostHoleInChild(HostHoleMode, ChildHostHole()); + CheckHostHoleInChild(MetadataCommitMode, ProbedHostHole()); CheckFailedCollectUnwatch(); #endif CheckVersionRanges(); @@ -1948,6 +2033,7 @@ int main() { CheckStoreAcrossImports(); CheckImportWindowFootprint(); CheckUnwatchedImport(); + CheckUnwatchHooks(); CheckCommittedRanges(); CheckAdoptLatestEpoch(); } catch (const std::exception& error) { 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/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 4d791ac7f7..09383ccc9a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" @@ -46,6 +47,18 @@ PFN_vkCmdCopyImageToBuffer realCopyImageToBuffer = nullptr; PFN_vkCmdCopyBufferToImage realCopyBufferToImage = nullptr; std::atomic imageToBufferCopies{0}; std::atomic bufferToImageCopies{0}; +std::atomic lastFilledImage{VK_NULL_HANDLE}; +PFN_vkAllocateMemory realAllocateMemory = nullptr; +std::atomic memoryAllocations{0}; +std::atomic lastAllocationBytes{0}; +std::atomic lastAllocationType{0}; + +VKAPI_ATTR VkResult VKAPI_CALL CountAllocateMemory(VkDevice device, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks* allocator, VkDeviceMemory* memory) { + memoryAllocations.fetch_add(1); + lastAllocationBytes = info->allocationSize; + lastAllocationType = info->memoryTypeIndex; + return realAllocateMemory(device, info, allocator, memory); +} VKAPI_ATTR void VKAPI_CALL CountImageToBuffer(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkBuffer buffer, std::uint32_t count, const VkBufferImageCopy* regions) { imageToBufferCopies.fetch_add(1); @@ -54,6 +67,7 @@ VKAPI_ATTR void VKAPI_CALL CountImageToBuffer(VkCommandBuffer commands, VkImage VKAPI_ATTR void VKAPI_CALL CountBufferToImage(VkCommandBuffer commands, VkBuffer buffer, VkImage image, VkImageLayout layout, std::uint32_t count, const VkBufferImageCopy* regions) { bufferToImageCopies.fetch_add(1); + lastFilledImage = image; realCopyBufferToImage(commands, buffer, image, layout, count, regions); } @@ -64,6 +78,10 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL CountingDeviceProc(VkDevice device, con realCopyImageToBuffer = reinterpret_cast(function); return reinterpret_cast(&CountImageToBuffer); } + if (std::strcmp(name, "vkAllocateMemory") == 0) { + realAllocateMemory = reinterpret_cast(function); + return reinterpret_cast(&CountAllocateMemory); + } if (std::strcmp(name, "vkCmdCopyBufferToImage") == 0) { realCopyBufferToImage = reinterpret_cast(function); return reinterpret_cast(&CountBufferToImage); @@ -434,6 +452,76 @@ void RecordedUploads(const Context& context, const WatchedBlock& block) { recorder.Sync(); } +void PooledPlaneCopies(const Context& context, const WatchedBlock& first, const WatchedBlock& second) { + ClearDepthSurfaces(context.device); + FillLayer(first.Data() + LayerBytes, 0.25f); + FillLayer(second.Data() + LayerBytes, 0.75f); + DepthSurfaceView(context, DepthTarget{first.Address(), 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); + DepthSurfaceView(context, DepthTarget{second.Address(), 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); + const auto sample = [&](const WatchedBlock& block, int round) { + lastFilledImage = VK_NULL_HANDLE; + bufferToImageCopies = 0; + Require(DepthSurfaceTexture(context, words, ArrayOver(block.Address()), components) != nullptr, "the 2D array over a depth surface was not sampled through its depth planes in round " + std::to_string(round)); + Require(DepthPlaneCopyCount() == 1, "the depth plane copies outgrew a budget that holds one in round " + std::to_string(round) + ", " + std::to_string(DepthPlaneCopyCount()) + " copies"); + Require(lastFilledImage.load() != VK_NULL_HANDLE && bufferToImageCopies.load() == 1, "a plane copy made again over a full budget was not filled from its surface once in round " + std::to_string(round) + ", " + std::to_string(bufferToImageCopies.load()) + " fills"); + }; + sample(first, 0); + sample(second, 0); + const auto keepsCopies = [&] { + if (context.bufferPool == nullptr) return false; + const VkMemoryAllocateInfo probe{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, nullptr, lastAllocationBytes.load(), lastAllocationType.load()}; + const auto memory = context.bufferPool->TakeImageMemory(probe, "vkAllocateMemory pool probe"); + const auto before = context.bufferPool->RetainedImages(); + context.bufferPool->PutImageMemory(memory, probe.allocationSize, probe.memoryTypeIndex); + const bool kept = context.bufferPool->RetainedImages() > before; + return kept; + }(); + memoryAllocations = 0; + for (int round = 1; round <= 3; ++round) { + sample(first, round); + sample(second, round); + } + if (!keepsCopies) { + std::printf("note: the device memory pool does not keep a block of a plane copy's size and memory type here (not device-tiered, or its heap over budget); the reuse of evicted copies' memory is not checked\n"); + } else { + Require(memoryAllocations.load() == 0, "plane copies evicted and made again over a full budget allocated device memory " + std::to_string(memoryAllocations.load()) + " times instead of taking the evicted copies' memory back"); + } + ClearDepthSurfaces(context.device); +} + +void SharedLayerRanges(const Context& context, const WatchedBlock& block) { + ClearDepthSurfaces(context.device); + auto resource = ArrayOver(block.Address()); + resource.depthOrLastArray = 3; + const auto geometry = DescribeSurface(resource); + for (std::uint32_t layer = 0; layer < 4; ++layer) { + DepthSurfaceView(context, DepthTarget{block.Address() + geometry.GuestLayerOffset(layer), 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); + } + DepthSurfaceView(context, DepthTarget{block.Address() + 16u * LayerBytes, 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); + const auto sample = [&](std::uint32_t lastArray, std::uint32_t baseArray, std::uint32_t fromSlices, std::uint32_t intoCopy) { + auto range = resource; + range.depthOrLastArray = lastArray; + range.baseArray = baseArray; + imageToBufferCopies = 0; + bufferToImageCopies = 0; + const auto texture = DepthSurfaceTexture(context, words, range, components); + const auto when = "layers " + std::to_string(baseArray) + "-" + std::to_string(lastArray); + Require(texture != nullptr, "the 2D array over depth surfaces was not sampled through its depth planes for " + when); + if (imageToBufferCopies.load() != fromSlices || bufferToImageCopies.load() != intoCopy) { + throw std::runtime_error("sampling " + when + " of the depth surfaces recorded " + std::to_string(imageToBufferCopies.load()) + " depth surface copies and " + std::to_string(bufferToImageCopies.load()) + " plane copy fills, expected " + std::to_string(fromSlices) + " and " + std::to_string(intoCopy)); + } + const auto view = texture->SampledViewRange(false); + Require(view.layers == lastArray + 1u - baseArray, "the view sampling " + when + " covers " + std::to_string(view.layers) + " layers, not the texture's own"); + return texture; + }; + sample(3, 0, 4, 1); + sample(2, 0, 0, 0); + sample(1, 0, 0, 0); + sample(2, 1, 0, 0); + sample(3, 0, 0, 0); + ClearDepthSurfaces(context.device); +} + void RunCubeGather(const Context& base, const WatchedBlock& block) { TextureDetiler detiler(base); auto context = base; @@ -508,6 +596,11 @@ void RunCubeTiledSlice(const Context& base, const WatchedBlock& block) { int main() { try { +#ifdef _WIN32 + _putenv_s("APS5_DEPTH_PLANE_COPY_MIB", "1"); +#else + setenv("APS5_DEPTH_PLANE_COPY_MIB", "1", 1); +#endif if (!AgcDriver::GuestMemory::WriteWatched()) { std::puts("skipped, guest memory has no write watch"); return Skipped; @@ -528,6 +621,11 @@ int main() { Require(AgcDriver::GuestMemory::Watched(block.Address(), BlockBytes), "depth plane guest layers: the guest block is not write-watched"); Run(context, block); RecordedUploads(context, block); + { + WatchedBlock second; + PooledPlaneCopies(context, block, second); + } + SharedLayerRanges(context, block); RunCubeGather(context, block); RunCubeTiledSlice(context, block); SparseGuestLayers(context); 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/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 5ee8abca21..2e4954f926 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -667,6 +667,19 @@ void RunBorderSwizzleTests(AgcDriver::VulkanDevice& device) { const auto activeSampler = tableImage ? RunDirectCode(device, TableCode, PointClamp, image) : RunDirectCode(device, TableSamplerCode, PointClamp, image); requireFault(activeSampler, "sampler table entry with an opaque black border", pair); } + auto secondOpaqueBlack = opaqueBlack; + secondOpaqueBlack[1] = (3u * 256u) << 12u; + SetRecord(0, opaqueBlack, image); + SetRecord(1, secondOpaqueBlack, image); + for (const std::uint32_t record : {0u, 1u}) { + Keys[0] = record; + Keys[1] = record; + requireFault(RunDirectCode(device, TableCode, PointClamp, image), "sampler table entry with an opaque black border", name + " rejected S# " + std::to_string(record) + " of two with the same reason"); + } + SetRecord(0, PointClamp, image); + SetRecord(1, opaqueBlack, image); + Keys[0] = 1; + Keys[1] = 0; } const auto cut = Run(device, TableCutCode, 1, Lanes, 64); Require(Holds(cut.samplerTable, opaqueBlack), "image table border: the EXEC-clear opaque-black S# was not included in the snapshot"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationModes.cpp index 913426408a..36e5ef3c0c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationModes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationModes.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -302,20 +303,187 @@ void RunRandom(AgcDriver::VulkanDevice& device, bool integer) { for (std::size_t index = RandomRows * RandomWords; index < Data.size(); ++index) Require(Data[index] == Sentinel, "random interpolation rows wrote beyond the rows"); } -int main() { +constexpr std::uint32_t ChainWords = 8; +constexpr std::uint32_t ChainGroup = 64; +alignas(256) std::array ChainData{}; + +RecompileResult CompileChain(std::span steps, bool half, bool integer, std::uint32_t iterations = 1u) { + IrProgram program; + const ShaderStageInputInfo inputs{}; + SpirvEmitterState state(program, inputs); + state.integerInterpolation = integer; + auto& module = state.module; + module.EmitCapability(spv::CapabilityShader); + module.EmitCapability(integer ? spv::CapabilityInt64 : spv::CapabilityFloat64); + module.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450); + const auto u32 = TypeU32(state); + const auto array = module.DecoratedType(spv::OpTypeRuntimeArray, {{spv::OpDecorate, {spv::DecorationArrayStride, 4u}}}, u32); + const auto structure = module.Type(spv::OpTypeStruct, array); + module.AddAnnotation(spv::OpDecorate, structure, spv::DecorationBlock); + module.AddAnnotation(spv::OpMemberDecorate, structure, 0u, spv::DecorationOffset, 0u); + const auto buffer = module.DefineGlobalVariable(module.Type(spv::OpTypePointer, spv::StorageClassStorageBuffer, structure), spv::StorageClassStorageBuffer); + module.AddAnnotation(spv::OpDecorate, buffer, spv::DecorationDescriptorSet, 0u); + module.AddAnnotation(spv::OpDecorate, buffer, spv::DecorationBinding, 0u); + const auto invocation = module.DefineGlobalVariable(TypePointer(state, spv::StorageClassInput, TypeU32Vector(state, 3u)), spv::StorageClassInput); + module.AddAnnotation(spv::OpDecorate, invocation, spv::DecorationBuiltIn, spv::BuiltInGlobalInvocationId); + const auto element = module.Type(spv::OpTypePointer, spv::StorageClassStorageBuffer, u32); + const auto main = module.AllocateId(); + const auto voidType = module.Type(spv::OpTypeVoid); + module.EmitEntryPoint(spv::ExecutionModelGLCompute, main, "main", {invocation}); + module.AddExecutionMode(main, spv::ExecutionModeLocalSize, ChainGroup, 1u, 1u); + module.AddFunction(spv::OpFunction, voidType, main, spv::FunctionControlMaskNone, module.Type(spv::OpTypeFunction, voidType)); + module.AddFunction(spv::OpLabel, module.AllocateId()); + const auto id = module.AllocateId(); + module.AddFunction(spv::OpLoad, TypeU32Vector(state, 3u), id, invocation); + const auto lane = module.AllocateId(); + module.AddFunction(spv::OpCompositeExtract, u32, lane, id, 0u); + const auto rowBase = Binary(state, spv::OpIMul, u32, lane, ConstantU32(state, ChainWords)); + const auto address = [&](std::uint32_t offset) { + const auto pointer = module.AllocateId(); + module.AddFunction(spv::OpAccessChain, element, pointer, buffer, ConstantU32(state, 0u), Binary(state, spv::OpIAdd, u32, rowBase, ConstantU32(state, offset))); + return pointer; + }; + const auto load = [&](std::uint32_t offset) { + const auto word = module.AllocateId(); + module.AddFunction(spv::OpLoad, u32, word, address(offset)); + return Unary(state, spv::OpBitcast, TypeF32(state), word); + }; + IrProgram constants; + IrBuilder builder(constants); + builder.SetInsertionPoint(constants.CreateBlock()); + const std::array deltas{load(0u), load(1u), load(2u), load(3u)}; + const auto coordinate = load(4u); + const auto chain = [&](std::uint32_t value) { + for (std::uint32_t step = 0; step < steps.size(); ++step) { + const auto* flags = &builder.Constant(steps[step]); + value = half ? EmitFPInterpolateF16(state, deltas[step % 4u], coordinate, value, flags) : EmitFPInterpolateF32(state, deltas[step % 4u], coordinate, value, flags); + } + return value; + }; + auto value = load(5u); + if (iterations == 1u) { + value = chain(value); + } else { + const auto entry = module.AllocateId(); + const auto header = module.AllocateId(); + const auto body = module.AllocateId(); + const auto next = module.AllocateId(); + const auto merge = module.AllocateId(); + const auto counter = module.AllocateId(); + const auto carried = module.AllocateId(); + const auto counted = module.AllocateId(); + const auto chained = module.AllocateId(); + module.AddFunction(spv::OpBranch, entry); + EmitLabel(state, entry); + module.AddFunction(spv::OpBranch, header); + EmitLabel(state, header); + module.AddFunction(spv::OpPhi, u32, counter, ConstantU32(state, 0u), entry, counted, next); + module.AddFunction(spv::OpPhi, TypeF32(state), carried, value, entry, chained, next); + module.AddFunction(spv::OpLoopMerge, merge, next, spv::LoopControlMaskNone); + module.AddFunction(spv::OpBranchConditional, Binary(state, spv::OpULessThan, TypeBool(state), counter, ConstantU32(state, iterations)), body, merge); + EmitLabel(state, body); + module.AddFunction(spv::OpCopyObject, TypeF32(state), chained, chain(carried)); + module.AddFunction(spv::OpBranch, next); + EmitLabel(state, next); + module.AddFunction(spv::OpIAdd, u32, counted, counter, ConstantU32(state, 1u)); + module.AddFunction(spv::OpBranch, header); + EmitLabel(state, merge); + value = carried; + } + module.AddFunction(spv::OpStore, address(6u), Unary(state, spv::OpBitcast, u32, value)); + module.AddFunction(spv::OpReturn); + module.AddFunction(spv::OpFunctionEnd); + RecompileResult result{}; + result.spirv = module.Finalize(); + const auto base = reinterpret_cast(ChainData.data()); + result.bindings.push_back({DescriptorKind::StorageBuffer, DescriptorRole::GuestBuffers, 0u, 0u, 1u, + {static_cast(base), static_cast((base >> 32u) & 0xffffu), static_cast(sizeof(ChainData)), 0x31016facu}, false}); + return result; +} + +void FillChains(std::uint32_t lanes, bool half, bool special) { + Generator generator{half ? 0x6b5f00000001ull : 0x6b5f00000002ull}; + const auto mantissa = [&]() { + const auto bits = generator.Pick(3u) == 0u ? 1u + generator.Pick(23u) : 23u; + return static_cast(generator.Next() & 0x7fffffu) & ~((1u << (23u - bits)) - 1u); + }; + const auto value = [&](std::uint32_t lowest, std::uint32_t count) { return (generator.Pick(2u) << 31u) | ((lowest + generator.Pick(count)) << 23u) | mantissa(); }; + ChainData.fill(Sentinel); + for (std::uint32_t lane = 0; lane < lanes; ++lane) { + auto* row = &ChainData[lane * ChainWords]; + for (std::uint32_t index = 0; index < 4u; ++index) row[index] = special && generator.Pick(8u) == 0u ? generator.Half() : special && generator.Pick(8u) == 0u ? generator.Single() : value(112u, 16u); + row[4] = special && generator.Pick(8u) == 0u ? generator.Single() : value(117u, 10u) & 0x7fffffffu; + row[5] = special && generator.Pick(8u) == 0u ? generator.Single() : value(110u, 24u); + } +} + +std::uint32_t ChainReference(const std::uint32_t* row, std::span steps, bool half) { + auto value = row[5]; + for (std::uint32_t step = 0; step < steps.size(); ++step) value = Reference(row[step % 4u], row[4], value, half, steps[step]); + return value; +} + +void RunChains(AgcDriver::VulkanDevice& device, bool integer) { + constexpr std::uint32_t lanes = 8192; + std::array steps{}; + for (std::uint32_t step = 0; step < steps.size(); ++step) steps[step] = ModeFlags[step % ModeFlags.size()] | (((step / ModeFlags.size()) % 4u) << InterpolationRound16Shift); + for (const bool half : {false, true}) { + FillChains(lanes, half, true); + device.Dispatch(CompileChain(steps, half, integer), lanes / ChainGroup, 1u, 1u); + device.WaitIdle(); + for (std::uint32_t lane = 0; lane < lanes; ++lane) { + const auto* row = &ChainData[lane * ChainWords]; + const auto expected = ChainReference(row, steps, half); + char message[200]; + std::snprintf(message, sizeof(message), "%s chain (%s), lane %u: 0x%08x 0x%08x 0x%08x 0x%08x * 0x%08x from 0x%08x gave 0x%08x, reference 0x%08x", + half ? "f16" : "f32", integer ? "integer" : "Float64", lane, row[0], row[1], row[2], row[3], row[4], row[5], row[6], expected); + Require(row[6] == expected, message); + } + } +} + +void Benchmark(AgcDriver::VulkanDevice& device, bool integer) { + constexpr std::uint32_t lanes = 1u << 16u; + constexpr std::uint32_t iterations = 1024; + const std::array steps{ModeFlags[0], ModeFlags[0], ModeFlags[0], ModeFlags[0]}; + for (const bool half : {false, true}) { + FillChains(lanes, half, false); + const auto program = CompileChain(steps, half, integer, iterations); + device.Dispatch(program, lanes / ChainGroup, 1u, 1u); + device.WaitIdle(); + std::array times{}; + for (auto& milliseconds : times) { + const auto start = std::chrono::steady_clock::now(); + device.Dispatch(program, lanes / ChainGroup, 1u, 1u); + device.WaitIdle(); + milliseconds = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + } + std::sort(times.begin(), times.end()); + std::printf("%s path, %u lanes x %u chained %s interpolations: median %.3f ms, min %.3f, max %.3f\n", integer ? "integer" : "Float64", lanes, iterations * static_cast(steps.size()), half ? "f16" : "f32", times[4], times.front(), times.back()); + } +} + +int main(int argc, char** argv) { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; const auto target = device->Target(); const bool float64 = std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityFloat64) != target.supportedCapabilities.end(); + if (argc == 2 && std::string(argv[1]) == "--benchmark") { + Benchmark(*device, true); + if (float64) Benchmark(*device, false); + return 0; + } for (std::uint32_t mode = 0; mode < ModeFlags.size(); ++mode) { Run(*device, mode, true); if (float64) Run(*device, mode, false); } RunRandom(*device, true); if (float64) RunRandom(*device, false); - std::puts(float64 ? "interpolation modes passed: 32 hardware rows and 2048 random rows against the reference in 32 float modes, 3 ops, integer and Float64 paths" - : "interpolation modes passed: 32 hardware rows and 2048 random rows against the reference in 32 float modes, 3 ops, integer path (the device has no Float64)"); + RunChains(*device, true); + if (float64) RunChains(*device, false); + std::puts(float64 ? "interpolation modes passed: 32 hardware rows and 2048 random rows against the reference in 32 float modes, 3 ops, 8192 chains of 64 f32 and f16 interpolations, integer and Float64 paths" + : "interpolation modes passed: 32 hardware rows and 2048 random rows against the reference in 32 float modes, 3 ops, 8192 chains of 64 f32 and f16 interpolations, integer path (the device has no Float64)"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetImportCreation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetImportCreation.cpp new file mode 100644 index 0000000000..adb40038d9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetImportCreation.cpp @@ -0,0 +1,474 @@ +#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 +#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 SettledBytes = 16 * BlockBytes; +constexpr std::size_t ResidentBytes = 32 * BlockBytes; +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 Rgba32FloatInfo = 0x8738u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32AbgrExport = 9; +constexpr VkColorComponentFlags RedGreen = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_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 PixelCode{}; + +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 at = 0; + PixelCode[at++] = 0x36000000u | (20u << 17u) | 0xffu; + PixelCode[at++] = 0x0000ffffu; + PixelCode[at++] = 0x2c000000u | (21u << 17u) | 0x90u; + PixelCode[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u); + PixelCode[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u); + PixelCode[at++] = 0x7e000280u | (22u << 17u); + PixelCode[at++] = 0x7e0002f2u | (23u << 17u); + PixelCode[at++] = 0xf800180fu; + PixelCode[at++] = 0x17161514u; + PixelCode[at++] = 0xbf810000u; + return std::span(PixelCode.data(), at); +} + +struct Block { + Block(std::size_t bytes, std::uintptr_t at, bool direct = false, std::size_t granule = 0, bool writable = true) : bytes(bytes), arena(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (arena && direct) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, 0x200000)); + Require(data != nullptr, "cannot allocate the direct memory of the color target"); + section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE | SEC_RESERVE, 0, static_cast(bytes), nullptr); + Require(section != nullptr, "cannot reserve the backing of the color target's direct memory"); + GuestArena::GuestArenaMapDirect_nid_postfix(data, bytes, section, 0, PAGE_READWRITE); + } else if (arena) { + data = static_cast(at != 0 ? GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix(at, bytes, BlockBytes) : GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, granule != 0 ? granule : bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + static_cast(direct); + static_cast(granule); + if (arena && at != 0) { + void* raw = mmap(reinterpret_cast(at), bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + data = static_cast(raw); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } 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"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, writable, true); + } + ~Block() { + 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); + } + if (section != nullptr) { + GuestArena::GuestArenaForgetSection_nid_postfix(section); + CloseHandle(section); + } +#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); } + bool Watched() const { return arena && AgcDriver::GuestMemory::Watched(Address(), bytes); } + std::size_t bytes; + bool arena; + std::uint8_t* data = nullptr; +#ifdef _WIN32 + HANDLE section = nullptr; +#endif +}; + +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; + Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && color.bytes == TargetBytes, "the target is not a linear R32G32B32A32 target"); + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const Block& block) { + const auto color = DecodeTarget(block); + 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()); + + const auto pixelCode = Program(); + 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 = RedGreen; + 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}, {Width, 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); +} + +void Seed(Block& block, const std::array& texel) { + for (std::size_t offset = 0; offset < TargetBytes; offset += sizeof(texel)) std::memcpy(block.data + offset, texel.data(), sizeof(texel)); +} + +void Check(const Block& block, const std::array& seed, const std::string& what) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + std::array texel{}; + std::memcpy(texel.data(), block.data + static_cast(y) * RowBytes + static_cast(x) * 16u, sizeof(texel)); + const std::array expected{static_cast(x), static_cast(y), seed[2], seed[3]}; + Require(texel == expected, what + ": 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]) + ")"); + } + } +} + +void ExpectRecorded(const AgcDriver::Graphics::Context& context, const Block& block, const std::string& what) { + const auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr && !recorder->Idle(), what + ": the draw into a linear target in registered, write-watched memory without a host import waited for the GPU instead of being recorded"); + Require(StorageTexture::FindPending(block.Address(), TargetBytes) != nullptr, what + ": the drawn linear target has no pending resident image"); + Require(AgcDriver::Graphics::ExistingHostImportFor(context, block.Address(), TargetBytes) != nullptr, what + ": the draw made no host import of the linear target"); + Require(block.Watched(), what + ": the linear target's host import took its memory out of the write watch"); +} + +void Created(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + constexpr std::array seed{100.0f, 200.0f, 300.0f, 400.0f}; + Block block(SettledBytes, 0); + Seed(block, seed); + Require(block.Watched(), "import created: the color target is not write-watched"); + Require(AgcDriver::Graphics::ExistingHostImportFor(context, block.Address(), TargetBytes) == nullptr, "import created: the color target has a host import before the draw"); + Draw(device, block); + ExpectRecorded(context, block, "import created"); + device.WaitIdle(); + Require(block.Watched(), "import created: the color target left the write watch after its store"); + Check(block, seed, "import created, after the device wait"); +} + +bool Reused(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + constexpr std::array first{100.0f, 200.0f, 300.0f, 400.0f}; + constexpr std::array second{500.0f, 600.0f, 700.0f, 800.0f}; + std::uint64_t address = 0; + { + Block block(SettledBytes, 0); + address = block.Address(); + Seed(block, first); + Draw(device, block); + ExpectRecorded(context, block, "reuse, first block"); + device.WaitIdle(); + Check(block, first, "reuse, first block"); + } + Block block(SettledBytes, static_cast(address)); + if (block.Address() != address) { + std::puts("linear target import reuse skipped: the second block was not mapped at the first one's address"); + return false; + } + Require(block.Watched(), "reuse: the block allocated at the freed block's address is not write-watched"); + Seed(block, second); + Require(block.Watched(), "reuse: the CPU's store took the second block out of the write watch"); + Draw(device, block); + ExpectRecorded(context, block, "reuse, second block"); + device.WaitIdle(); + Check(block, second, "reuse, the draw into the second block did not start from the CPU's bytes"); + return true; +} + +#ifdef _WIN32 +AgcDriver::Graphics::HostImportRequesterCounts ColorTargetCounts() { + return AgcDriver::Graphics::HostImportCounters().requesters[static_cast(AgcDriver::Graphics::ImportRequester::ColorTarget)]; +} + +void Direct(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + constexpr std::array seed{1300.0f, 1400.0f, 1500.0f, 1600.0f}; + Block block(ResidentBytes, 0, true); + Seed(block, seed); + Require(block.Watched(), "direct memory: the color target is not write-watched"); + Require(AgcDriver::Graphics::ExistingHostImportFor(context, block.Address(), TargetBytes) == nullptr, "direct memory: the color target has a host import before the draw"); + Draw(device, block); + ExpectRecorded(context, block, "direct memory"); + device.WaitIdle(); + Require(block.Watched(), "direct memory: the color target left the write watch after its store"); + Check(block, seed, "direct memory, after the device wait"); +} + +void PrivateGate(AgcDriver::VulkanDevice& device) { + using AgcDriver::Graphics::ImportRequester; + using AgcDriver::Graphics::ImportWatch; + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + Block direct(ResidentBytes / 4, 0, true); + Block flexible(2 * BlockBytes, 0); + GuestArena::GuestArenaCommit_nid_postfix(flexible.data + BlockBytes, BlockBytes, PAGE_READONLY, BlockBytes); + const auto decided = AgcDriver::Graphics::PrepareImportWatch(context); + const auto before = ColorTargetCounts(); + AgcDriver::Graphics::SetImportWatch(context, ImportWatch::Unwatch); + const auto* compared = AgcDriver::Graphics::HostImportFor(context, direct.Address(), TargetBytes, ImportRequester::ColorTarget); + AgcDriver::Graphics::SetImportWatch(context, ImportWatch::Watch); + const auto* readOnly = AgcDriver::Graphics::HostImportFor(context, flexible.Address(), TargetBytes, ImportRequester::ColorTarget); + const auto refused = ColorTargetCounts(); + const auto* watched = AgcDriver::Graphics::HostImportFor(context, direct.Address(), TargetBytes, ImportRequester::ColorTarget); + const auto made = ColorTargetCounts(); + AgcDriver::Graphics::SetImportWatch(context, decided); + Require(compared == nullptr, "private gate: a color target's import of direct memory under the settle floor was made where it would be compared"); + Require(readOnly == nullptr && refused.made == before.made && refused.refused == before.refused && refused.failed == before.failed, "private gate: a color target's import was made of private memory committed read-only while registered writable, or a refused color target import was counted"); + Require(watched != nullptr && made.made == before.made + 1 && made.failed == before.failed, "private gate: a color target's import of direct memory registered writable was not made where imports stay watched"); +} +#endif + +void OutsideWatch(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + constexpr std::array seed{1700.0f, 1800.0f, 1900.0f, 2000.0f}; + Block block(SettledBytes, 0); + Seed(block, seed); + AgcDriver::GuestMemory::Unwatch(block.Address() + SettledBytes - BlockBytes, BlockBytes); + Require(AgcDriver::GuestMemory::Watched(block.Address(), TargetBytes) && !block.Watched(), "run outside the write watch: the target is not watched, or the rest of its block still is"); + Draw(device, block); + const auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder == nullptr || recorder->Idle(), "run outside the write watch: a linear target whose import run has a block outside the write watch was drawn into a resident image"); + Require(AgcDriver::Graphics::ExistingHostImportFor(context, block.Address(), TargetBytes) == nullptr, "run outside the write watch: the draw made a host import over a block outside the write watch"); + Require(AgcDriver::GuestMemory::Watched(block.Address(), TargetBytes), "run outside the write watch: the draw took the target out of the write watch"); + device.WaitIdle(); + Check(block, seed, "run outside the write watch, after the device wait"); +} + +void ReadOnlyRegistry(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + constexpr std::array seed{2500.0f, 2600.0f, 2700.0f, 2800.0f}; + Block block(SettledBytes, 0, false, 0, false); + Seed(block, seed); + Require(block.Watched(), "range registered read-only: the color target is not write-watched"); + Draw(device, block); + const auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder == nullptr || recorder->Idle(), "range registered read-only: a linear target in memory the guest registered read-only was drawn into a resident image"); + Require(AgcDriver::Graphics::ExistingHostImportFor(context, block.Address(), TargetBytes) == nullptr, "range registered read-only: the draw made a host import of memory the guest registered read-only"); + device.WaitIdle(); + Check(block, seed, "range registered read-only, after the device wait"); +} + +#ifdef _WIN32 +void Rewatched(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + constexpr std::array seed{2100.0f, 2200.0f, 2300.0f, 2400.0f}; + Block block(SettledBytes, 0, false, BlockBytes); + Seed(block, seed); + auto* last = block.data + SettledBytes - BlockBytes; + AgcDriver::GuestMemory::Unwatch(block.Address() + SettledBytes - BlockBytes, BlockBytes); + Draw(device, block); + const auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder == nullptr || recorder->Idle(), "run back in the write watch: the draw before the block came back was drawn into a resident image"); + device.WaitIdle(); + GuestArena::GuestArenaReset_nid_postfix(last, BlockBytes); + GuestArena::GuestArenaCommit_nid_postfix(last, BlockBytes, PAGE_READWRITE, BlockBytes); + Require(block.Watched(), "run back in the write watch: committing the block again did not bring it back into the write watch"); + Draw(device, block); + ExpectRecorded(context, block, "run back in the write watch, a refused run whose block came back"); + device.WaitIdle(); + Check(block, seed, "run back in the write watch, after the device wait"); +} +#endif + +void Compared(AgcDriver::VulkanDevice& device) { + using AgcDriver::Graphics::ImportWatch; + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + constexpr std::array seed{900.0f, 1000.0f, 1100.0f, 1200.0f}; + const auto decided = AgcDriver::Graphics::PrepareImportWatch(context); + AgcDriver::Graphics::SetImportWatch(context, ImportWatch::Unwatch); + Block block(BlockBytes, 0); + Seed(block, seed); + Require(block.Watched(), "compared import: the color target is not write-watched"); + Draw(device, block); + const auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder == nullptr || recorder->Idle(), "compared import: a linear target whose host import would be compared was drawn into a resident image"); + Require(AgcDriver::Graphics::ExistingHostImportFor(context, block.Address(), TargetBytes) == nullptr, "compared import: the draw made a host import that leaves the target's memory compared"); + Require(StorageTexture::FindPending(block.Address(), TargetBytes) == nullptr, "compared import: the drawn linear target has a pending resident image"); + Require(block.Watched(), "compared import: the draw took the color target out of the write watch"); + device.WaitIdle(); + Check(block, seed, "compared import, after the device wait"); + AgcDriver::Graphics::SetImportWatch(context, decided); +} + +} + +int main(int argc, char** argv) { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!AgcDriver::GuestMemory::WriteWatched()) { + std::puts("skipped, guest memory is not write-watched"); + return VulkanTestSkipped; + } + const std::string_view only = argc > 1 ? argv[1] : ""; +#ifndef _WIN32 + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + const auto decided = AgcDriver::Graphics::PrepareImportWatch(context); + AgcDriver::Graphics::SetImportWatch(context, AgcDriver::Graphics::ImportWatch::Watch); + struct Restore { + const AgcDriver::Graphics::Context& context; + AgcDriver::Graphics::ImportWatch decided; + ~Restore() { AgcDriver::Graphics::SetImportWatch(context, decided); } + } restore{context, decided}; +#endif + Block probe(SettledBytes, 0); + if (AgcDriver::Graphics::HostImportFor(AgcDriver::VulkanDeviceTestAccess::ContextOf(*device), probe.Address(), probe.bytes) == nullptr) { + std::puts("skipped, host imports are unavailable"); + return VulkanTestSkipped; + } + const bool watched = probe.Watched(); +#ifndef _WIN32 + Require(watched, "a host import of write-watched memory left the write watch although imports are set to stay watched"); +#endif + if (!watched) std::puts("linear target import creation: a host import of write-watched memory does not stay watched on this device; only the compared cases run"); + if (watched && (only.empty() || only == "--created")) Created(*device); + if (watched && (only.empty() || only == "--reused") && Reused(*device)) std::puts("linear target import reuse passed"); +#ifdef _WIN32 + if (watched && (only.empty() || only == "--direct")) Direct(*device); + if (only.empty() || only == "--private-gate") PrivateGate(*device); +#endif + if (watched && (only.empty() || only == "--outside-watch")) OutsideWatch(*device); + if (watched && (only.empty() || only == "--read-only-registry")) ReadOnlyRegistry(*device); +#ifdef _WIN32 + if (watched && (only.empty() || only == "--rewatched")) Rewatched(*device); +#endif + if (only.empty() || only == "--compared") Compared(*device); + std::puts("linear target import creation 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 b837e0411d..3d3531a577 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -704,6 +704,48 @@ void CheckDepthAttachmentAlias() { 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() { @@ -1337,6 +1379,7 @@ 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/PipelinedDma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp index 7a62e1e7cf..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(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 82c999000d..94b7c946eb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -1485,6 +1485,11 @@ void DispatchAheadPrewarm() { int main(int argc, char** argv) { try { +#ifdef _WIN32 + HANDLE diagnosticError = nullptr; + Require(DuplicateHandle(GetCurrentProcess(), GetStdHandle(STD_ERROR_HANDLE), GetCurrentProcess(), &diagnosticError, 0, FALSE, DUPLICATE_SAME_ACCESS) != 0, "cannot retain the native stderr handle"); + Require(SetStdHandle(STD_ERROR_HANDLE, diagnosticError) != 0, "cannot retain native diagnostics during stderr capture"); +#endif 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]) == "--dispatch-ahead-prewarm" || std::string_view(argv[1]) == "--lazy")), "invalid test arguments"); 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/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/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/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/libSceAjm.native/CMakeLists.txt b/core/libs/prx/libSceAjm.native/CMakeLists.txt index 49fc25b039..8734851197 100644 --- a/core/libs/prx/libSceAjm.native/CMakeLists.txt +++ b/core/libs/prx/libSceAjm.native/CMakeLists.txt @@ -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) @@ -57,6 +60,11 @@ if(BUILD_TESTING) target_link_libraries(guest_ajm_tests PRIVATE libSceAjm.native libc) configure_windows_unwind(guest_ajm_tests) add_test(NAME guest_ajm COMMAND guest_ajm_tests) + add_test_executable(guest_ajm_aac_multichannel_tests tests/AacMultichannel.cpp) + target_include_directories(guest_ajm_aac_multichannel_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ajm_aac_multichannel_tests PRIVATE libSceAjm.native libc) + configure_windows_unwind(guest_ajm_aac_multichannel_tests) + add_test(NAME guest_ajm_aac_multichannel COMMAND guest_ajm_aac_multichannel_tests) add_test_executable(guest_ajm_vibration_tests ${LIBS_INCLUDE_DIR}/tests/GuestAjmVibration.cpp) target_include_directories(guest_ajm_vibration_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_ajm_vibration_tests PRIVATE libSceAjm.native libc) @@ -75,7 +83,7 @@ if(BUILD_TESTING) add_test(NAME guest_ajm_teardown COMMAND guest_ajm_teardown_tests $) endif() if(WIN32) - set_tests_properties(guest_ajm guest_ajm_vibration guest_ajm_wvorbis PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_ajm guest_ajm_aac_multichannel guest_ajm_vibration guest_ajm_wvorbis PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 72284a8281..6f4e548a72 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -234,10 +234,14 @@ struct PackedJob { }; static_assert(sizeof(PackedJob) == 32); -int Append(AjmBatchInfo* info, const JobHeader& header, const AjmBuffer* inputs, const AjmBuffer* outputs) { +int JobRejected(const AjmBatchInfo* info, const JobHeader& header) { if (!info || !info->p_buffer) return SCE_AJM_ERROR_INVALID_PARAMETER; if (header.inputCount > 0xffu || header.outputCount > 0xffu || header.parameterSize > sizeof(header.parameters) || header.sidebandSize > 0xffffffffu) return SCE_AJM_ERROR_INVALID_PARAMETER; - if ((header.inputCount && !inputs) || (header.outputCount && !outputs)) NotImplemented_nid_no_patch("AJM batch job with a null buffer list and a nonzero buffer count"); + return 0; +} + +int Append(AjmBatchInfo* info, const JobHeader& header, const AjmBuffer* inputs, const AjmBuffer* outputs) { + if (const int rejected = JobRejected(info, header)) return rejected; const std::size_t parameterBytes = (header.parameterSize + 7u) & ~std::size_t{7}; const std::size_t bytes = sizeof(PackedJob) + (header.inputCount + header.outputCount) * sizeof(AjmBuffer) + parameterBytes; if (info->offset > info->size || bytes > info->size - info->offset) return SCE_AJM_ERROR_OUT_OF_RESOURCES; @@ -254,6 +258,12 @@ int Append(AjmBatchInfo* info, const JobHeader& header, const AjmBuffer* inputs, return 0; } +int CheckedAppend(AjmBatchInfo* info, const JobHeader& header, const AjmBuffer* inputs, const AjmBuffer* outputs) { + if (const int rejected = JobRejected(info, header)) return rejected; + if ((header.inputCount && !inputs) || (header.outputCount && !outputs)) NotImplemented_nid_no_patch("AJM batch job with a null buffer list and a nonzero buffer count"); + return Append(info, header, inputs, outputs); +} + JobHeader MakeHeader(JobKind kind, std::uint32_t instance, void* sideband, std::uint64_t sidebandSize) { JobHeader header{}; header.kind = kind; @@ -707,7 +717,9 @@ SidebandFormat CurrentFormat(const Instance& instance) { 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}; + case CODEC_M4AAC: + if (instance.aacChannels == 3 || instance.aacChannels == 5) NotImplemented_nid_no_patch("AJM M4AAC format with an unknown three- or five-channel speaker mask"); + return {instance.aacChannels, ChannelMask(instance.aacChannels), instance.aacSampleRate, PcmEncoding(instance), instance.aacBitrate, 0}; case CODEC_PCM: return {instance.pcmChannels, ChannelMask(instance.pcmChannels), instance.pcmSampleRate, PcmEncoding(instance), 0, 0}; default: throw std::runtime_error("AJM: the format of codec " + std::to_string(instance.codec) + " is not implemented"); } @@ -1423,7 +1435,7 @@ std::int32_t InitializeAac(Instance& instance, const std::uint8_t* parameters, s if (config != M4AAC_CONFIG_ADTS && config != M4AAC_CONFIG_RAW) return AJM_RESULT_INVALID_PARAMETER; if (config == M4AAC_CONFIG_RAW && frequencyIndex >= 12) return AJM_RESULT_INVALID_PARAMETER; const std::uint32_t rawChannels = AacRawChannels(instance); - if (config == M4AAC_CONFIG_RAW && rawChannels != 1 && rawChannels != 2 && rawChannels != 6) throw std::runtime_error("AJM M4AAC: raw initialization of an instance created for " + std::to_string(rawChannels) + " channels is not implemented (only 1, 2 and 6)"); + if (config == M4AAC_CONFIG_RAW && (rawChannels < 1 || rawChannels > 6)) throw std::runtime_error("AJM M4AAC: raw initialization of an instance created for " + std::to_string(rawChannels) + " channels is not implemented (only 1 through 6)"); instance.aacConfig = config; instance.aacFrequencyIndex = frequencyIndex; OpenAac(instance); @@ -1472,8 +1484,11 @@ std::size_t EmittedSamples(const Instance& instance, std::size_t samples) { bool AacLayoutImplemented(const AVChannelLayout& layout) { const AVChannelLayout mono = AV_CHANNEL_LAYOUT_MONO; const AVChannelLayout stereo = AV_CHANNEL_LAYOUT_STEREO; + const AVChannelLayout three = AV_CHANNEL_LAYOUT_SURROUND; + const AVChannelLayout four = AV_CHANNEL_LAYOUT_4POINT0; + const AVChannelLayout five = AV_CHANNEL_LAYOUT_5POINT0_BACK; const AVChannelLayout surround = AV_CHANNEL_LAYOUT_5POINT1_BACK; - return av_channel_layout_compare(&layout, &mono) == 0 || av_channel_layout_compare(&layout, &stereo) == 0 || av_channel_layout_compare(&layout, &surround) == 0; + return av_channel_layout_compare(&layout, &mono) == 0 || av_channel_layout_compare(&layout, &stereo) == 0 || av_channel_layout_compare(&layout, &three) == 0 || av_channel_layout_compare(&layout, &four) == 0 || av_channel_layout_compare(&layout, &five) == 0 || av_channel_layout_compare(&layout, &surround) == 0; } std::size_t AdtsFrameBytes(const Instance& instance, const std::uint8_t* data) { @@ -1482,7 +1497,7 @@ std::size_t AdtsFrameBytes(const Instance& instance, const std::uint8_t* data) { if (header.profile != 1) throw std::runtime_error("AJM M4AAC: ADTS profile " + std::to_string(header.profile) + " is not implemented (only AAC LC)"); if (header.rawBlocks != 0) throw std::runtime_error("AJM M4AAC: ADTS frames with more than one raw data block are not implemented"); if (header.frequencyIndex >= 12) throw std::runtime_error("AJM M4AAC: ADTS sampling frequency index " + std::to_string(header.frequencyIndex) + " is not implemented"); - if (header.channelConfig != 1 && header.channelConfig != 2 && header.channelConfig != 6) throw std::runtime_error("AJM M4AAC: ADTS channel configuration " + std::to_string(header.channelConfig) + " is not implemented (only 1, 2 and 6)"); + if (header.channelConfig < 1 || header.channelConfig > 6) throw std::runtime_error("AJM M4AAC: ADTS channel configuration " + std::to_string(header.channelConfig) + " is not implemented (only 1 through 6)"); const std::uint32_t limit = InstanceChannels(instance); if (limit != 0 && header.channelConfig > limit) throw std::runtime_error("AJM M4AAC: an ADTS stream of " + std::to_string(header.channelConfig) + " channels on an instance created for " + std::to_string(limit) + " is not implemented"); return header.frameBytes; @@ -1495,6 +1510,17 @@ void RunAac(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c PcmOutputs pcmOutputs(outputs, job.outputCount); const auto encoding = PcmEncoding(instance); if (encoding > 2) throw std::runtime_error("AJM M4AAC: PCM encoding " + std::to_string(encoding) + " is not implemented"); + if (job.flags & SIDEBAND_FORMAT) { + static_cast(CurrentFormat(instance)); + if (raw && !input.empty() && (AacRawChannels(instance) == 3 || AacRawChannels(instance) == 5)) NotImplemented_nid_no_patch("AJM M4AAC format with an unknown three- or five-channel speaker mask"); + for (std::size_t offset = 0; !raw && input.size() - offset >= ADTS_HEADER_BYTES;) { + AdtsHeader header{}; + if (!ParseAdtsHeader(input.data() + offset, header) || header.frameBytes > input.size() - offset) break; + if (header.channelConfig == 3 || header.channelConfig == 5) NotImplemented_nid_no_patch("AJM M4AAC format with an unknown three- or five-channel speaker mask"); + if ((job.flags & RUN_MULTIPLE_FRAMES) == 0) break; + offset += header.frameBytes; + } + } const std::size_t sampleBytes = encoding == 0 ? sizeof(std::int16_t) : sizeof(std::int32_t); std::unique_ptr packet(av_packet_alloc(), [](AVPacket* p) { av_packet_free(&p); }); std::unique_ptr decoded(av_frame_alloc(), [](AVFrame* f) { av_frame_free(&f); }); @@ -1533,7 +1559,7 @@ void RunAac(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c if (++received > 1) throw std::runtime_error("AJM M4AAC: one input frame decoded to more than one output frame (raw input holding several access units is not implemented)"); const auto format = static_cast(decoded->format); if (format != AV_SAMPLE_FMT_FLTP) throw std::runtime_error("AJM M4AAC: FFmpeg sample format " + std::to_string(decoded->format) + " is not converted"); - if (!AacLayoutImplemented(decoded->ch_layout)) throw std::runtime_error("AJM M4AAC: a decoded layout of " + std::to_string(decoded->ch_layout.nb_channels) + " channels is not implemented (only mono, stereo and 5.1)"); + if (!AacLayoutImplemented(decoded->ch_layout)) throw std::runtime_error("AJM M4AAC: decoded channel layout is not implemented"); const bool heaac = instance.aac->profile == AV_PROFILE_AAC_HE || instance.aac->profile == AV_PROFILE_AAC_HE_V2; if (heaac && (instance.flags & M4AAC_ENABLE_SBR_DECODE) == 0) throw std::runtime_error("AJM M4AAC: an HE-AAC (SBR or PS) stream on an instance without ENABLE_SBR_DECODE is not implemented"); const auto channels = static_cast(decoded->ch_layout.nb_channels); @@ -1567,7 +1593,7 @@ void RunAac(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c AJM_TRACE("[ajm] instance %u m4aac run flags 0x%llx: %zu input bytes, %zu output bytes -> result 0x%x, %u frames, consumed %zu, produced %zu, %u channels, %u Hz, %u bps, heaac %d, total samples %llu\n", job.instance, static_cast(job.flags), input.size(), pcmOutputs.capacity, static_cast(result), frames, consumed, pcmOutputs.produced, instance.aacChannels, instance.aacSampleRate, instance.aacBitrate, instance.aacHeaac ? 1 : 0, static_cast(instance.totalDecodedSamples)); - WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, CurrentFormat(instance)); + WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, (job.flags & SIDEBAND_FORMAT) ? CurrentFormat(instance) : SidebandFormat{}); } void ClearContext(Instance& instance) { @@ -1694,7 +1720,7 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out NotImplemented_nid_no_patch("AJM RUN_GET_CODEC_INFO for a codec other than ATRAC9 and M4AAC"); } else if (job.inputCount == 0 && job.outputCount == 0) { AJM_TRACE("[ajm] instance %u run flags 0x%llx without buffers, sideband %llu bytes\n", job.instance, static_cast(job.flags), static_cast(job.sidebandSize)); - WriteRunSideband(job, *instance, 0, 0, 0, 0, CurrentFormat(*instance)); + WriteRunSideband(job, *instance, 0, 0, 0, 0, (job.flags & SIDEBAND_FORMAT) ? CurrentFormat(*instance) : SidebandFormat{}); } else if (instance->codec == CODEC_AT9) { RunAt9(*instance, job, inputs, outputs); } else if (instance->codec == CODEC_MP3) { @@ -2006,7 +2032,7 @@ int APS5_VABI sceAjmBatchJobControl(AjmBatchInfo* info, uint32_t instance, uint6 header.flags = flags; header.inputCount = sideband_input_size != 0 ? 1 : 0; const AjmBuffer input{const_cast(sideband_input), sideband_input_size}; - return Append(info, header, &input, nullptr); + return CheckedAppend(info, header, &input, nullptr); } int APS5_VABI sceAjmBatchJobRunSplit(AjmBatchInfo* info, 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) { @@ -2014,7 +2040,7 @@ int APS5_VABI sceAjmBatchJobRunSplit(AjmBatchInfo* info, uint32_t instance, uint header.flags = flags; header.inputCount = static_cast(input_buffers_num); header.outputCount = static_cast(output_buffers_num); - return Append(info, header, input_buffers, output_buffers); + return CheckedAppend(info, header, input_buffers, output_buffers); } int APS5_VABI sceAjmBatchJobRun(AjmBatchInfo* info, 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) { @@ -2072,7 +2098,7 @@ int APS5_VABI sceAjmBatchJobGetCodecInfo(AjmBatchInfo* info, uint32_t instance, int APS5_VABI sceAjmBatchJobGetStatistics(AjmBatchInfo* info, float interval, void* result) { (void)interval; - return Append(info, MakeHeader(JobKind::GetStatistics, 0, result, 24), nullptr, nullptr); + return CheckedAppend(info, MakeHeader(JobKind::GetStatistics, 0, result, 24), nullptr, nullptr); } int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int priority, AjmBatchError* error, uint32_t* batch) { diff --git a/core/libs/prx/libSceAjm.native/tests/AacMultichannel.cpp b/core/libs/prx/libSceAjm.native/tests/AacMultichannel.cpp new file mode 100644 index 0000000000..8e6feb9eb9 --- /dev/null +++ b/core/libs/prx/libSceAjm.native/tests/AacMultichannel.cpp @@ -0,0 +1,491 @@ +#include "prx/libSceAjm.native/libSceAjm.native.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +namespace { + +constexpr std::uint64_t Stream = 1ull << 47; +constexpr std::uint64_t Format = 1ull << 46; +constexpr std::uint64_t Multiple = 1ull << 12; +constexpr std::uint64_t Nondelay = 1ull << 33; +constexpr std::array Frequencies{311, 677, 1013, 1423, 1871}; + +void Require(bool value, int line) { + if (!value) { + std::fprintf(stderr, "AAC multichannel check failed at line %d\n", line); + std::abort(); + } +} + +#define CHECK(value) Require((value), __LINE__) + +constexpr std::string_view Aac3 = + "fff14cc02fdffcdc004c61766336312e332e31303000025ca65b69683d390b44995ade70e6f5adf1cb5532eaf28f9924" + "93cac312c57496c5b6b11a6a9de6ae35c5b15a7ac9f7699415b0e652cfc0a0479da710cde5c8c3763e4113033b02890f" + "c15124fd57b4f9d70b956538ef45ebdd87869d8e8d6cfad9f5b5663a363a35f9aaa52a94aa52a949a5268928928928c9" + "44944944944944940940944944944944944940c0c0c0c0c8914b2cb2cb2cb2cb2c514514514514514514514511095069" + "00bc4000276cb07666c0cdf5eff8ae7ad4a85c92b547724924911016561bcedd4b8ebd449b15dbded3988e8fe5f4b4ba" + "244aa33ad6956d2b5a535ac26ae6ae6ae6c31c31919db0c70c70c709b09b0c64c649a49a49a499d9d85a46919d9e8747" + "6761676767676769db1c3511b0737dd3dfd7e35af32f85c93b4e9e724892e2101f70b62618fb964aa255165a499ab3c9" + "7d17e49489546732d456894a6b4c942784666732d04d34a1270e1c094b4d2830303030303220606366ea60ba6be0fff1" + "4cc03c9ffc011a96daca5db4598f1654975659272fff8beff7eb87a6af57e3ffdbff6ea71c6b5ad7f7fffb5fe7f5bd75" + "abd6bfa7ffd9ffbf135d6b5ad07f7286c8b9a69969bf85ae743ef9682a30585a9a75327681d7e76b7f3d46b7f3f39adf" + "cfce6b9bf9ff3f38d737f3fe7fcc6bd6fe7fcff989baa1036743c478e079a6389b257d229fe914ff464453abe9f48a29" + "e7fa7d228a79fe9f4e78a213a87d3e9f48a213ce3e9f4fa45114f3aadafb6c0e870938716133049c0909381212702424" + "24852db2000400c03c13b64aedb22d38b23dd94ed9faf9e386d5deb5ffa6febceb5c6af5afffb99f7eb5ae357ad7ffda" + "e7efedd715abd684fa0dee51451445bf37b4a0462e9edd90bc7cf7200624eee6e50665dc1a5060606060606777770606" + "94181997770606360c0c0ceee0c0c0c0c0c0dbaf50cee243bc753c670f54e0d936c45fcbd5ac7f2fe5ac7f2fe5ae63f9" + "7f2fe5ac1fcbf97f2d60fe5fcbf96b9bdd3a3952cb29b941818181818181818181818181a5ec95db64a7a43eec8cfd64" + "bad2fff0ffbfb71a756a6b5fdffb7fd7eae3ee3ffc3fb7fdfeb407f1fdfff6f6eb8f3c6b8fb85a950463514506452804" + "4e9a17878226117c9547397c82425dc18199cea02e1a8bd5115e387e0f3fc1e765510c953cff2e6fde9fde9fde97bf7a" + "5ee0fff14cc027dffc011cf4195624311f4242d092defede35affa77cf3faffb7f5e38d5fdf9b5cb8f2e1c6a5c922480" + "0c0f1f3638f9bc3c3703839393c7a4504ff15afdf5949dda0a69971414d38d05fd5517f35f1987bfbc2327c363dfdc86" + "4cc61efee07c6600c9f7f703e33019c801f0d81ee407c3603a40ff19827dc81fe3304fb903fc3613ee41ff86c27dc83f" + "f0d84fb901f0d84f6901f0d84fb9047c3613ee411f0d84fb910bd029cb1b077cff5ff6fbaf8f8deaf534d24971aad249" + "2492440ffc666cbe3e3e1bddefefa07c7c363dfde13fc7c36efbfb90f8f809f7d00ff0d877df407fe1b13858d0887422" + "1d5089babf7f5fa7aff1c75f79d385c4b91d37ad49722493ef03befef0bef9d5efa3407fe3e03befee1ff8f8327dfdc8" + "ff1f007be803e3e00f7d007c7c01fc4f3607f03f8f8479bc462c279bc462c2e0fff14cc0243ffc00f8330be5d12b2aa9" + "3f4fff0fff0fffe1ededd71c0915112220ec0fa3cf3cf3cf30e738d2c783cc79e79efa6ba28a28a28a28a28a28a28a28" + "a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28" + "a28a28a28a28fd747ea8a28a28a28a28a28a28a28a28a28a28211a0801e0000004295f4641d4015fdffedff1ffdffebc" + "71abbd5c92449239e490888359d2a53cf3cf7399e79e79e79ed745145145145145145145145145145145145145145145" + "143e95f5e82673df7f3fff6fe7ffc7febfaeae6492490f38888883e812f3cf3c94b0d1b05145145143cf3cf3cf6979ef" + "dbcf7ef4bdfbfdfeff7fbfdfeff7fb79e03030303030303030303030303030303efff14cc0283ffc010030992e261a4d" + "3155ed3f3ffc7fdbfffdf5f7357249044880ececececec475d35b1dae97a7c5252ed5b39f7535d20c0c0c0c0c0c0c0c0" + "c0c6e5965965965965965965965965965965965965965965965965965965965965965965965965965965965965965965" + "965965965965965965965965965965965211a0801fffc0004316b4a41043f6ffebfc7fe7ff7e35abbd5dc9249244a722" + "49d0e295dc6e3c78fcbe44279e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e7" + "9e79e79e79de8b3a540e8503a140e8903a273cf7dfcfffdaf9ffdfff2fd757724924911ea8952489007f5f5f5f53e109" + "5ca114514514514514514514514514514514514514514514514514514514514514418181818181818303030303030303" + "030303030303030303030303030303030338fff14cc0271ffc01002fa1362a2b1c83a73f5c73ade7fe3fd7ffe1f8f32b" + "d5fc4a4dbaa449008220eeb738d8b8d8b3a6e362ce243dd1b703608187a683e1ae31ca7e2b85d7ab3ee5d076ec6e5561" + "b14eb66aa94b3b3b3b3b3e38638635e35d35d35d35d35b3b3b3b3b3b3b3b3b3bcf3cf3c423410038070000842d887444" + "1d09074221d100dfa7ffdbfe3ff7ffbf1c6b57c690924480e75dc481cf220c54514514501efdfeff6f3cf3cf3cf3cf3c" + "f3cf3cf3cf3cf3dfbfdfeff7fbfdfeff7fbfdfeff7fbfdfeff7fbfdbcf3cf3cf3cf3cf3cf0079e79e79e79e79e79e79e" + "79ed73981e931e1d17874de1d120744df7cf7effff6bf6ffdfff2ff4e35dc924935269eb08888d20c51a3468e38d6b5b" + "e134d34c060606060606060606060606060606060606060734d33cd34cb9a69a67030303030303039780fff14cc02d7f" + "fc0146341914c45ea4d52fed5f77cfeb1cc923711110f5841033b3d35d35d35ebdc6465a6c1ea9cef72f96a149e2f74c" + "ced987b651d03dbc3dbc0e20e20e20e20e20e20e20e20e20e20e20e20e20e20e20606060606060606060606060606060" + "606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606061" + "08d0600fff03ffe993e97af7f7fc7172e4924448891088001e5b4d1e3c18181818181818181818181818181818181818" + "18181818181818181818181818181934d34d34d34d34d34d34d34d34d34d34d34d34d34d34d34d306060606060606060" + "606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060" + "606060606060606060606060606060606060606060606060606060606060606060606060606065312859d265666ff39f" + "aebda4911225000007c9e79e79e4c33cf300000007"; + +constexpr std::string_view Aac4 = + "fff14d00409ffcdc004c61766336312e332e31303000025ca55b69284d410b42e6dadaddeb5ad8d54c9907ca4924f2b1" + "8962b0682cf54ed354ef3571ae2d8ae7ed53eed3282b61cca5ea699439da710cde5c8c3763e4113033b02890fc15124f" + "d57b4f9d70b956538ec4dca76b53b1d1ad9f5b3eb6acc746c746bf3554a552954a552934a4d125125125125125125125" + "030303030303030303031b366c191229659658a28a28a28a28a28a28a28a22129c505960e8cd795cf9ad56f8d4a96b92" + "223ad2492488815552b796b9d35268898c373758cf5b5b6c9b693191122532d129454891030303030368606060637a58" + "181b43220606366c181912206366cd8322448911b366e89b1af2b2b3c7afc6b5e65dda6ed247749224b88418e18e18e1" + "357357360d86495d15d15cd5b618c98c94c94c98e13613613618e18c98c98c934934933b3b3b3b3b3b3b3b3a3b3b3b0b" + "3b3b3a3a3b3b0b0b0b019229d6ea3a174c4773fc746a9a937170c9891019249d4840ed6ed5b6ad91854c953254c95a9a" + "5a9a5a9a62d4ee159f75b6e1ef2fa8ff4acd1e0c6bb6013bb87e16549a6b5c9cb3c9dcc791646e594e2ae366b8d9ab33" + "d1b1cd5f9abf3554a55293449a24a24a24a24a24a060606060606366c181912294d9b372cb2cb2cb2cb1451451451451" + "45145145145145145145145145145145145145145145145145145145145145145145145cfff14d00505ffc011a96dac9" + "5db447bb25dd94edfff7bfd3efd70f4d5eafc7ffdcffbfd75c71ad6b5fdfffe97fd7efe75d6af5afe9fff77fcfd75aeb" + "5ad683fb942835145064dfc2d73a1f7cba738467aa3afced6fe7a8d6fe7e735bf9f9cd737f3fe7e71ae6fe7fcff98d7a" + "dfcff9ff313754206ce8788f1c0a830106e257d229fe8c88a757d3e91453cff4fa4514f3fd3e9cf1442750fa7d3e9144" + "279c7d3e9f48a229e755b5f6d6d731c38b099824e04849c0909381212124296d89db2576d94eec9649653b67ebe78b6d" + "5deb5ff8dfd79d6b8d5eb5fff733efd6b5c6af5afffb5cfdfad6b8d5eb427d06f728a28a21bf37b4a0462e9edd8acd66" + "f37794181893831b06cddc18181893b830303033b83030303033bbb830303030366eee0c0c0c0c0c0ceefbaf50cee243" + "bc7539985ad846d6cfa738fa6be7007d3e9f460012121255dc24242424abbbb848484849577770909090909baeefee9d" + "1ca372830303030303030303030303030303034bd92bb6c92f20d083b22bc871649794da5ffe1ff7f6e34ead4d6bfbff" + "6ffadffd7ffc3fb7fdd9fdfff2eff7e3cf1ae3ee16a54118d45141914a0113a685e1e0898441212ee0c0cce750170d45" + "ea88af1c2caa2195c06079019103220646e8191ba06292db512bb6c977647bb25dbffe93febedc3e7535af6fffcfd7fd" + "fdb8f3ad6b5f3fffa7cffd7db8eb8d6b5ebffeaeffefedc75c6b8b195759c0c6a283585cb30dd4f9f7c68e12083094f4" + "5ccf088b983cf4517cb99e7a28be5f279e645117cbe5ccf3cc8a22f97cbe4f3cf45145f2f97c9dfdb9d95f81c3abb049" + "48c518ce3ea9c5ea8eb1fcb66b1fcbf96b98fe5fcbf96b987f2fe5fcb5cc3f97f2fe5af5b9b6560208a288a28a28a28a" + "2e3145145170fff14d00249ffc011af38b3a120eb1efce2fe7c73cff1ff5fad752f52eeee5bcf5a92e49244077fe3e3e" + "20cf9fdfddd0d8f7f7847c7c3633fbe8193e0c33fb90c9f0d8f7f7847c7c187bfb81f1f00193efee010bce29cafb077c" + "ff5ff6fbaf8f8deaf4e2b4b971aa69249249203ff199b2f8f8f86f77bfbe81f1f0d8f7f784ff1f0da7dfdc0f8f809f7d" + "01ff8cc1df7d13a500091f4241d08875421655fbfafd3d7f8e3afbce1ad5c92e3a2f5725c8924fb877dfde17df3abdf4" + "680ffc7c04fbe807f84fbfb91fe3e0c3dfdc0f8f809f7f707f8f803e00f7d03f81fc7c23cde23161019279c58d7b7935" + "cf29bf19ffe1ff3e7eee25eaeede4d25c92492027dfdfdc9fc9f0da5df7f7f705efa3dca1c9f198cefbe803e1b0efbe8" + "0ffc660efbe80ffc661cfff14d0035bffc00fa330be5d1332aa93edffe1ffe1fffc3dbcf5d70108911107587d1e79e79" + "e798739c6963c1e63cc79efa6ba28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a" + "28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28fd747ea8a28a28a28a28a28a28a28a28a2" + "8a282119c4295f4641d4011fdffedff1ffdffebc71abbd49224911cf26a11106b3a54a79e79ee7fa7d3e9f4fa6bd7451" + "45145145145145145145145145145145145145145145143e17cda277cefbf9fffb7f3ffe3ff5fd4972492224912220ee" + "81294bcf252c3471a5e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79" + "e79e79e79e79e79e79e79e79e79e79e79e7bf7fbfdfef4bcf3cf3cf3cf3cf3dfbfdfeff7fbfdbda5e79e79e79e7bf7fb" + "fdfeff7fbfdfeff7fbfdfeff7fbfdbcf3cf3cf3cf3c05e46325f5242d09555595953d7ffe6ffdfffca6ee5dc8f8b5a49" + "3f1340181914b2cb2cb2ca0df2606fab3f4feb7d2df26f936869659659659659659659659659659659658a28a28a28a2" + "8a28a28a28a2e0fff14d003d9ffc0102308be20aa4fcfff87ff87fffcf8f3d6ae5c89224480f97cbe5f2f94f3c294a47" + "309d53abe53fcbe5f2f97ca28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a2" + "8a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a22119c4325f52077edffd7f8ffcffefc6b577a97" + "24924473c912480ddb830b366cd830303030303030303030303030303030303030303120606bc0c0d7af5ebd7af5ebd7" + "9e93e873df3dfcfffdaf9ffdfff2fc692492489224444402c183060cb20654c30e181818181818181818181818181818" + "181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818" + "181818181818181818181818181818181818181830303030303030303030303030303030303030303030303030303030" + "303030303030303030303030303030303030303030303030303030303030303030303030300bc8c82f94aacacaccefed" + "fffcbffc7ff5b97772e4b9249224a1f4e779e79e79e79e0f04861079e79e79e79e79e79e79e79e79e79e79e79e79e79e" + "79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e" + "79e79e79e79e79e79e79e79e79e79e79e48038fff14d003abffc01022f91364431134c420a27afb7fc7ffaf117a97248" + "ea922750048484848484849c885cc8d82061e9a0ded0d712cb8964f1dd1212127024e04848484dd092b75e79e79e79e7" + "9e79e79e79e79e79e79e79e79e79e79e78846710a5b1210c43a2013f3fff6ff8ffdffefc6af5c7164489122244838c1e" + "91062a28a28a28a3e9f4fa3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cc79e79e79e7980079e79e798" + "035ebd731af5ebd7af5ebd7ac03cf3cf3cf3cf7eff7fbfdfeff6f3cf3cf3cf3cf3dae7303d25fd881d0a0742e79e7bf7" + "fffb5fb7fefff97dfa92492489a001f0f00628d1a3471c6b5ad6f2cb2cb2cb2cb2ca665033328199940c0cc818181999" + "65940c0e5940c0c0c0c0c0c0c0c0c0c0c0c0c0c0e5033e330028ab1a61fffeab32b33733ffecffefe65dddc972492488" + "880f7af5ebc3060cbc19af11a4639204eec22d53965d72b940303030b06160c0cbc185af0606160c2d7832f5ebc18181" + "9783030b5ebc18197af5e0c2d7af5ebc185af5ebd783032f5ebd7af06060616bd7af06060606060616bc181818181818" + "1818185af5ebd7af5e0c0c0c0c0c0c0c18185af5ebd7af5e0c0c0c0c0c0c0c0c2c18185af5ebd7830303030303030303" + "0303030303030338fff14d00437ffc014a338b48bd12aaab2ab37f6fdf8b97248891222107887d1e79e79e79e79ec827" + "54f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cffafd754f3cf3cf3cf38606065ebd78303030303030" + "30303030b5ebd783030303030303030303030303030b5e0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c2d7af060" + "606060606060606060606060606060606060606060606108ce2ad3e86fd7bfe38b497244444488884203196d3478f064" + "d31e0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c" + "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c" + "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c" + "0c0c0c0c0c0c0c0ca82bfbb44accacfce7ebad4b92491128000ee83e8f3cf3cf2619e79e79e79e79e79e79e79e79e79f" + "f5fa9e79e7fd7ea79e79e7fd7ebaa70348338be44168482552aaa95dfedfaf57772e244911110f10f97ca79e79e79e79" + "e79cf39db783cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3" + "cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf7eff7fbfdfeff7a5e79e79e79e79e03030303030" + "303030303030303030303fdfeff7a5e79e7b80"; + +constexpr std::string_view Aac5 = + "fff14d4042dffcdc004c61766336312e332e31303000025ca45ba9084d539b5e669ceb57546a953707ca4924906258ac" + "1a0b3d53b4d53ba3b48e2d8ad3d64f7f766d4409547d4d32873b4e219bcb9186ec7c8226067605121f82a043e8dd6bcc" + "5ad716c56cac4dca76b53b1d1ad9f5b3eb6acc746c746bf3554a552954a4d293449a24a24a24a24a24a24a24a0606063" + "60c0c0c8914a6cd9b9659659659484a613d6664a275c677e5e353ad222e4888ed492492220ce5ae74d71ef154fa42630" + "dcdd633d455c4e95cd5b618f6f5eaa7553236135735733b3b48c0c0c0c6f4b0303686440c0c6cd830322440c6cd9b064" + "489122366cdd4a674e462b2b3c7afc6b5e65dda5cb4b8e69224f322401212134d284c2dd7af5eb830322a00c6cd9b060" + "6448911b366cda2448a506366cdca8a5965965360c8914b29b372cb14514514514514514514514514514514514514514" + "514514514514514514514514514514446530a24cea487a130a299dfedf7f6ea5dcb921e5249249f05809090909090909" + "09090909094a12e7ebd7af5c181818181818d8303030303030303030303030303031b360c0c8914b2cb2cb2c51451451" + "45145145145145145145145145151a63c241d3907c641028a5678fd2ff7e35a972ee3c52493c8b012121212121212121" + "2121212121212138547115deb9b06452831b360c0c0c0c0c6c1818181819122941818181914514514514514514514514" + "514514514517fff14d40615ffc011a96dac95db647bb25dd94edfff7bfd3efd70f4d5eaf3ffe97fdfebae38d6b5afeff" + "ff4bfebf7f3aeb57ad7f4fffbbfe7ebad75ad6b41fdca141a8a28340abb0cf3dc4a7da174e708cf5475f9dadfcf51adf" + "cfce6b7f3f39ae6fe7fcfce35cdfcff9ff31af5bf9ff3fe626ea840d9d0f11e381506020dc4afa453fd19114eafa7d22" + "8a79fe9f48a29e7fa7d39e2884ea1f4fa7d22884f38fa7d3e914453ceab6bedadae638716133049c0909381212702424" + "24852db13b64aedb29dd92c32ca76cfd7cf1c36aef5affd37f5e75ae357ad7ffdccfbf5ad71abd6bffed73f7eb5ae357" + "ad09f41bdc8d145116fcded28118ba7b762b35bcacbca0cf2833b9b94199770694181818181819ddddc181a5060665dd" + "c1818d8303033bb8303030303036ebd433b890ef1d4e6611a861b5b0df46187d35bc00fa7d1e07f2fe5ae63f97f2fe5a" + "c1fcbf97f2d60fe5fcbf96b9bdd3a3952ca0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0d2f6bc93db644fd43ee57f56bd26d2" + "fff0f8ffefc69d5a9ad7f7feddd29fffe47ff5fe2ea80ff9fef5bfff5e3cf1ae3ee16a54116145141914a0113a685e1e" + "8402425dc181aff3d9ee397e384865510ca00303c80c881910323740c8dd234b6c45512bb6c977647bb25dbffe93febe" + "dc3e7535af6fffcfd7fdfdb8f3ad6b5f3fffa7cffd7db8eb8d6b5ebffeaeffefedc75c6b8b195759c0c6a283585cb30d" + "d4f9f7c68e12c606129e8b99e11173079e8a2f9733cf4517cbe4f3cc8a22f97cb99e7991445f2f97c9e79e8a28be5f2f" + "93bfb73b2bf0387576092939ce319c7d538bd51d63f96cd63f97f2d731fcbf97f2d7ac3f97f2fe5ae61fcbf97f2d7adc" + "db2b0104514451451451451718a28a289f2895db644f92daac89f2578eab25cea7ff1fff6bffd7eba7cf17abfd7ffe2f" + "3c7d75e78d55d7ffc4e3ebaf3c7fc65f9fffb9fff6fffe1f5d79e35561ad78ec879b9959f799b3ef33623c3a94a42a54" + "f3cf3aa754eae69e79e79fe53f34f3cff2e69fe53cfcd3cf88f0eb97795cbbca94a294a9e79e754ea9d53cf3cf3cf3cf" + "3cf3cfdbb3f08ec54f389e79e79e79f8fff14d402c5ffc011af319c4bd090b4a41d1905efce55e78e79fe3febc7c65de" + "a5ddddc79f0e25c9f172580306666666427c1f07c1f00c10264007777c1c0fe3e1b19fdf40c9f0619fdc864f86c7bfbc" + "23e3e1b4fbfb90f8f800c9f7f723fc7c04fbe80327c660efbc207c3613ee442f30a72bebddf3fd7fdbeebe3e37abd38a" + "d4972daa69249249207fe33365f1f1f0deef7f7d03e3e1b1efef09fe3e1b4fbfb90f8f80efbfb87fe33077dfde74afaf" + "654f5ebf4f5fe38ebef389a5e92e3a12e25c89240efbfbc2fbe757be8d01ff8f809f7f707f8f809f7f784ff1f061efee" + "0ff1f064fbfb8237984895f5ee726b9de5e7be7ff87fcf9fbce2eef51e91a4b9249240efbfbfb93ff1f0da53efefee0b" + "dcbdca1c9f198cefbfb81f1983befa09ff8cc1df7d12257d7b738ef26bc55f8dfe9ff1d7fd7e3ae25e9ef772d7249248" + "1ff8f833befa0f93e333b9fdc9f1901c8423e3e0c3dfdf40f8f830f7f721f1f0d8e0fff14d40493ffc00fa330be5d133" + "2aa93edffe1ffe1fffc3dbcf5d70221111107587d1e79e79e798739c6963c1e63cc79efa6ba28a28a28a28a28a28a28a" + "28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a" + "28a28a28a28a28fd747ea8a28a28a28a28a28a28a28a28a28a28211984295f4641d400dfdffedff1ffdffebc71abbd4b" + "4892239e4e211106b3a54a79e79ee7fa7d3e9f4fa6bfa6ba28a28a28a28a28a28a28a28a28a28a28a28a28a28a28a287" + "c2f9b42ef9df7f3fff6fe7ffc7febfaa492492488912220ee094a5e792961a38d2f3cf3cf3cf3cf3cf3cf3cf3cf3cf3c" + "f3cf3da5e79efdfede79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e7bf7fbfdfef4" + "bcf3cf3cf3cf3cf3dfbfdfeff7fbfdbda5e79e79e79e7bf7fbfdfeff7fbfdfeff7fbfdfeff7fbfdbcf3cf3cf3cf3c463" + "30792beb0062ab2b32bbf5fff9bff7fff7497a972df16b4924907cbe5f2f9658a28a2891951303184514514514514514" + "514514514514514514514514f81511634b3fffee6673cf7afffb19ffe5c4b977249244840381818181e207181a30348d" + "7049ec0319832f843360036670303030303940c0cc8199940c0c0e503030303030303030303030303320606060607281" + "8181818199030303030303030303030303030303030303030303030303030303332ca060606060606060606060606060" + "60606060606060606060606060606060666503030303030303030303030303030303030303030303039780fff14d4047" + "9ffc0102309936448e8c04dff7ffb7fff75d6aee4892248734076767676767676743dc66d71ea303486b5c3a457730b1" + "2145f460606bc0c0c0c0c0c0d2cb2cb2cb2cb2cb2cb2cb2cb2cb2cb2cb2cb2cb2cb2cb2cb2cb2cb2cb2cb2cb2cb2cb2c" + "b2cb2cb2cb2cb2cb2cb2ca42330864bea40cfdbffaff1ff9ffdf8d6aef52e49249239e4892406edc1859b366c1818181" + "81818181818181818181818181818181818903035e0606bc0c0d7af03035ebcf49f3b9e7befe7ffed7cffefff97eb6b9" + "1248922448840583060c19640ca9861c3030303030303030303030303030303030303030303030303030303030303030" + "303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030" + "303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030" + "3030303030303030303030308c660f05f108c04aaacaacccefedfffcbffc7ff5b97772e4b9249244a1f4e779e79e79e7" + "9e0f04861079e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e798f3cf3cf303cf3c" + "f3c03cf3cf3cc01e79e79e78079e79e79e79e79e79e79e79e980d7af5ebd7af5eb009f02922c68af3107c241f090742f" + "fffecdf3e3b9fff17afff2e25ddcb92c1ec90ca01eef8a24448891114bde1b600caa61f0847800ea9e59659659659659" + "659659659659659659659659659659659659659628a28a28a28a28a28a28a28a28a28a28a28b80fff14d404c1ffc0102" + "2f911654390f48440a267f5ff8fff5e2af5772e23aa49c4401212121212121270130b991b040c3d341bda1ae259712c9" + "e3a121212702424242424242424add092b779de79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e" + "78846610a5f188744017e7ffedff1ffbffdf8d5eb8e2c891244489107183d220c545145145147d3e9f479e79e79e79e7" + "9e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e67ede79e79e79e79e6001e79e79e79e7bf" + "7fbfdfef4bcf3cf3cf3cf3cf3dfbfdfeff7fbfdbcf3cf3cf3cf3cf6b9cc0f49af4a07428173cf3dfbfffdafdbff7ffcb" + "efe52e2492246803cbc031468d1a38e35ad6b746072cb2ca06060606665033328199940c0cc818181818181818181cb2" + "81818181ca0606060606072cb2cb2818181818181818181818181818181ca463309f00a2ac68a23a1dfffeab2b3332ab" + "ffecffefecbd5cb924969244203debd7860c18306066bc46918e4813bb08b54e5975cae50060606160c2c18181818181" + "8181858303030303030b0606065e0c0c2d7af7ebf53cf3febaa79e79e79e79e79e79e79e79e79e79e79e79e79e79e79e" + "79e79e79e79e79c303030303030303030303030303030606060606060606060606060606060606160c0c2d7af5ebc181" + "818181818181818181818181818193e014200d0b644368883e52200dffffe2b9e7bc7ffdaf7fff8ed25c694e94579901" + "c2424ae38e32e28916963ab4e99880df0a4cf2b842cd001b450160c0c0d2cb2cb2cb2cb2cb2cb2cb2cb2c51451451451" + "4514514514514514514514514514514514514514514517fff14d4040fffc014a330b48bd0aaaaab2b37f6fdf8b972491" + "1122220788fa3cf3cf3cf3cf3d904ea9e79e79e79e79e79e79e79e79e79ff5fa9e79e79ff5faea9e79e79e7fd7ebaa79" + "e79e79e79c303032f5ebc181818181818181818185af5ebc18181818181818181818181818185af06060606060606060" + "6060606060606060606060606060606060606060606060606060606060606060606060606060606108cc2ad3e76fd7bf" + "e38b5c924488888888420632da68f1e0c983030303030303030303030303030303030303030303030303030303030303" + "030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303" + "030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303" + "03030303030303030303030303030303032a0a089f668559959f9cfd75a977248908a000ee1f479e79e7930cf3cf3cf3" + "cf3cf3cf3cf3860606060606065e0c0c0c0c0c0c0c0c0c0c0c0c2d7830303030303030b5ebc46330a42bebca5552ab39" + "fdbf5eaeee5c91ef1110840f97ca79e79e79e79e79cf39db783cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf52a5f4e" + "41d380a159bfecff3c6ae5c79c924bf88420090909090909090909090909090909094a12f6a9cdca6e59659659659659" + "659659659659658a28a28a28a2e0"; + +struct Sideband { + std::int32_t result; + std::int32_t internalResult; + std::int32_t consumed; + std::int32_t produced; + std::uint64_t total; + std::array extra; +}; + +static_assert(offsetof(Sideband, extra) == 24); + +std::vector FromHex(std::string_view text) { + const auto digit = [](char c) { return c <= '9' ? c - '0' : c - 'a' + 10; }; + std::vector result; + for (std::size_t i = 0; i < text.size(); i += 2) result.push_back(static_cast(digit(text[i]) * 16 + digit(text[i + 1]))); + return result; +} + +std::vector> Frames(const std::vector& stream) { + std::vector> result; + for (std::size_t offset = 0; offset < stream.size();) { + CHECK(stream.size() - offset >= 7); + const std::size_t size = ((std::size_t{stream[offset + 3]} & 3) << 11) | (std::size_t{stream[offset + 4]} << 3) | (stream[offset + 5] >> 5); + CHECK(size >= 7 && size <= stream.size() - offset); + result.emplace_back(stream.data() + offset, size); + offset += size; + } + CHECK(result.size() == 7); + return result; +} + +class Batch { +public: + explicit Batch(std::uint32_t owner) : context(owner) { + CHECK(sceAjmBatchInitialize(storage.data(), storage.size(), &info) == 0); + } + + void Submit() { + AjmBatchError error{}; + std::uint32_t id = 0; + CHECK(sceAjmBatchStart(context, &info, 0, &error, &id) == 0); + CHECK(sceAjmBatchWait(context, id, 0, &error) == 0); + } + + AjmBatchInfo info{}; + +private: + std::uint32_t context; + std::array storage{}; +}; + +std::uint32_t Create(std::uint32_t context, std::uint32_t channels, bool raw, bool floating, bool delay = false) { + std::uint32_t instance = 0; + CHECK(sceAjmInstanceCreate(context, 2, (std::uint64_t{channels} << 3) | (floating ? 2ull << 7 : 0) | (delay ? 0 : Nondelay), &instance) == 0); + Batch batch(context); + std::array result{-1, -1}; + const std::array config{raw ? 2u : 1u, 3u}; + CHECK(sceAjmBatchJobInitialize(&batch.info, instance, config.data(), sizeof(config), result.data()) == 0); + batch.Submit(); + CHECK(result[0] == 0 && result[1] == 0); + return instance; +} + +Sideband Run(std::uint32_t context, std::uint32_t instance, std::span input, void* output, std::size_t capacity, std::uint64_t flags = 0) { + Batch batch(context); + Sideband result{}; + CHECK(sceAjmBatchJobRun(&batch.info, instance, flags | Stream, input.data(), input.size(), output, capacity, &result, sizeof(result)) == 0); + batch.Submit(); + return result; +} + +void CheckSignal(const std::vector& pcm, std::size_t channels) { + CHECK(pcm.size() == 7 * 1024 * channels); + for (std::size_t channel = 0; channel < channels; ++channel) { + for (std::size_t frequency = 0; frequency < channels; ++frequency) { + double cross = 0, signalEnergy = 0, referenceEnergy = 0; + for (std::size_t i = 2048; i < 6144; ++i) { + const double signal = pcm[i * channels + channel]; + const double reference = std::sin(2 * 3.14159265358979323846 * Frequencies[frequency] * (i - 1024) / 48000.0); + cross += signal * reference; + signalEnergy += signal * signal; + referenceEnergy += reference * reference; + } + const double correlation = cross / std::sqrt(signalEnergy * referenceEnergy); + if (!(channel == frequency ? correlation > 0.9 : std::fabs(correlation) < 0.05)) std::fprintf(stderr, "channels %zu channel %zu reference %zu correlation %f\n", channels, channel, frequency, correlation); + CHECK(channel == frequency ? correlation > 0.9 : std::fabs(correlation) < 0.05); + } + } +} + +std::vector Decode(std::uint32_t context, std::uint32_t channels, bool raw, const std::vector& input, bool floating, bool delay = false) { + const auto instance = Create(context, channels, raw, floating, delay); + const auto frames = Frames(input); + const std::size_t sampleBytes = floating ? 4 : 2; + std::vector pcm; + for (std::size_t index = 0; index < frames.size(); ++index) { + auto packet = frames[index]; + if (raw) packet = packet.subspan(7); + std::vector storage(1024 * channels + 2, 0x5a5a5a5a); + auto* output = reinterpret_cast(storage.data()) + 4; + const auto result = Run(context, instance, packet, output, 1024 * channels * sampleBytes, channels == 4 ? Format : 0); + const std::size_t produced = delay && index < 2 ? 0 : 1024 * channels; + CHECK(result.result == 0 && result.internalResult == 0 && result.consumed == static_cast(packet.size())); + CHECK(result.produced == static_cast(produced * sampleBytes)); + CHECK(result.total == (delay ? (index < 2 ? 0 : index - 1) : index + 1) * 1024); + if (channels == 4 && produced != 0) CHECK(result.extra[0] == 4 && result.extra[1] == 0x33 && result.extra[2] == 48000 && result.extra[3] == (floating ? 2u : 0u)); + CHECK(storage.front() == 0x5a5a5a5a); + CHECK(storage[(1024 * channels * sampleBytes + 4) / 4] == 0x5a5a5a5a); + for (std::size_t sample = 0; sample < produced; ++sample) { + if (floating) { + float value = 0; + std::memcpy(&value, output + sample * 4, 4); + pcm.push_back(value); + } else { + std::int16_t value = 0; + std::memcpy(&value, output + sample * 2, 2); + pcm.push_back(value / 32768.0f); + } + } + } + Batch query(context); + std::array codec{0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}; + CHECK(sceAjmBatchJobGetCodecInfo(&query.info, instance, codec.data(), sizeof(codec)) == 0); + query.Submit(); + CHECK(std::all_of(codec.begin(), codec.end(), [](auto v) { return v == 0; })); + std::array format{}; + format.fill(0x5a5a5a5a); + Batch info(context); + CHECK(sceAjmBatchJobGetInfo(&info.info, instance, format.data()) == 0); + if (channels == 4) { + info.Submit(); + CHECK(format[0] == 0 && format[1] == 0 && format[2] == 4 && format[3] == 0x33 && format[4] == 48000 && format[5] == (floating ? 2u : 0u)); + } else { + try { info.Submit(); } + catch (const std::runtime_error&) { + CHECK(std::all_of(format.begin(), format.end(), [](auto v) { return v == 0x5a5a5a5a; })); + } + } + CHECK(sceAjmInstanceDestroy(context, instance) == 0); + return pcm; +} + +void CheckFormatGuard(std::uint32_t context, std::uint32_t channels, bool raw, const std::vector& input) { + auto instance = Create(context, channels, raw, true); + const auto frame = Frames(input).front(); + const auto packet = raw ? frame.subspan(7) : frame; + std::vector output(1024 * channels, -123.0f); + Sideband status; + std::memset(&status, 0x5a, sizeof(status)); + const auto original = status; + Batch batch(context); + CHECK(sceAjmBatchJobRun(&batch.info, instance, Stream | Format, packet.data(), packet.size(), output.data(), output.size() * 4, &status, sizeof(status)) == 0); + bool threw = false; + try { batch.Submit(); } catch (const std::runtime_error&) { threw = true; } + if (threw) { + CHECK(std::memcmp(&status, &original, sizeof(status)) == 0); + CHECK(std::all_of(output.begin(), output.end(), [](auto v) { return v == -123.0f; })); + } else { + CHECK(sceAjmInstanceDestroy(context, instance) == 0); + instance = Create(context, channels, raw, true); + } + std::fill(output.begin(), output.end(), -123.0f); + const auto normal = Run(context, instance, packet, output.data(), output.size() * 4); + const auto fresh = Create(context, channels, raw, true); + std::vector expected(output.size()); + const auto freshResult = Run(context, fresh, packet, expected.data(), expected.size() * 4); + CHECK(normal.result == 0 && freshResult.result == 0 && output == expected); + CHECK(sceAjmInstanceDestroy(context, fresh) == 0); + CHECK(sceAjmInstanceDestroy(context, instance) == 0); +} + +void CheckMixedFormatGuard(std::uint32_t context, const std::vector& four, const std::vector& three) { + const auto first = Frames(four).front(); + const auto second = Frames(three).front(); + std::vector mixed(first.begin(), first.end()); + mixed.insert(mixed.end(), second.begin(), second.end()); + auto instance = Create(context, 5, false, true); + std::vector output(2048 * 5, -123.0f); + Sideband status; + std::memset(&status, 0x5a, sizeof(status)); + const auto original = status; + Batch batch(context); + CHECK(sceAjmBatchJobRun(&batch.info, instance, Stream | Format | Multiple, mixed.data(), mixed.size(), output.data(), output.size() * 4, &status, sizeof(status)) == 0); + bool threw = false; + try { batch.Submit(); } catch (const std::runtime_error&) { threw = true; } + if (threw) { + CHECK(std::memcmp(&status, &original, sizeof(status)) == 0); + CHECK(std::all_of(output.begin(), output.end(), [](auto v) { return v == -123.0f; })); + } else { + CHECK(sceAjmInstanceDestroy(context, instance) == 0); + instance = Create(context, 5, false, true); + } + std::fill(output.begin(), output.end(), -123.0f); + const auto result = Run(context, instance, first, output.data(), output.size() * 4, Format); + const auto fresh = Create(context, 5, false, true); + std::vector expected(output.size(), -123.0f); + const auto freshResult = Run(context, fresh, first, expected.data(), expected.size() * 4, Format); + CHECK(result.result == 0 && freshResult.result == 0 && output == expected); + CHECK(sceAjmInstanceDestroy(context, fresh) == 0); + CHECK(sceAjmInstanceDestroy(context, instance) == 0); +} + +void CheckBulkAndReset(std::uint32_t context, std::uint32_t channels, const std::vector& input, const std::vector& reference) { + const auto instance = Create(context, channels, false, true); + std::vector output(reference.size()); + const auto result = Run(context, instance, input, output.data(), output.size() * 4, Multiple); + CHECK(result.result == 0 && result.consumed == static_cast(input.size()) && result.produced == static_cast(output.size() * 4)); + CHECK(result.total == 7 * 1024 && result.extra[0] == 7 && output == reference); + Batch reset(context); + std::array status{-1, -1}; + CHECK(sceAjmBatchJobClearContext(&reset.info, instance, status.data()) == 0); + reset.Submit(); + CHECK(status[0] == 0 && status[1] == 0); + const auto restart = Run(context, instance, input, output.data(), output.size() * 4, Multiple); + CHECK(restart.result == 0 && restart.total == 7 * 1024 && restart.produced == static_cast(output.size() * 4)); + CheckSignal(output, channels); + CHECK(sceAjmInstanceDestroy(context, instance) == 0); +} + +} // namespace + +int main() { + std::uint32_t context = 0; + CHECK(sceAjmInitialize(0, &context) == 0); + const std::array, 3> streams{FromHex(Aac3), FromHex(Aac4), FromHex(Aac5)}; + for (std::uint32_t channels = 3; channels <= 5; ++channels) { + const auto& input = streams[channels - 3]; + const auto floating = Decode(context, channels, false, input, true); + CheckSignal(floating, channels); + CheckBulkAndReset(context, channels, input, floating); + CHECK(Decode(context, channels, true, input, true) == floating); + const auto integer = Decode(context, channels, false, input, false); + CHECK(integer.size() == floating.size()); + for (std::size_t i = 0; i < integer.size(); ++i) CHECK(std::fabs(integer[i] - floating[i]) <= 0.5f / 32768); + const auto delayed = Decode(context, channels, false, input, true, true); + CHECK(delayed.size() == floating.size() - 2048 * channels); + CHECK(std::equal(delayed.begin(), delayed.end(), floating.begin() + 2048 * channels)); + if (channels != 4) { + CheckFormatGuard(context, channels, false, input); + CheckFormatGuard(context, channels, true, input); + } + } + CheckMixedFormatGuard(context, streams[1], streams[0]); + CHECK(sceAjmFinalize(context) == 0); +} diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index 0eddd25c80..dba49fc5ca 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -434,9 +434,13 @@ int APS5_VABI sceAudio3dAudioOutOutputs(AudioOutOutputParam* param, uint32_t num return 0; } -int APS5_VABI sceAudio3dStrError(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +const char* APS5_VABI sceAudio3dStrError(std::int32_t error) { + static constexpr const char* descriptions[]{ + "port does not exist", "object does not exist", "invalid parameter", + "out of memory", "out of resources", "not ready" + }; + const auto index = static_cast(error) - 0x80EA0002u; + return index < std::size(descriptions) ? descriptions[index] : "(unknown)"; } int APS5_VABI sceAudio3dPortOpenEx(void) { diff --git a/core/libs/prx/libSceAudio3d/include/Audio3d.hpp b/core/libs/prx/libSceAudio3d/include/Audio3d.hpp index 54c1e127b3..e85ae355e0 100644 --- a/core/libs/prx/libSceAudio3d/include/Audio3d.hpp +++ b/core/libs/prx/libSceAudio3d/include/Audio3d.hpp @@ -32,7 +32,7 @@ int APS5_VABI sceAudio3dAudioOutOpen(uint32_t portId, int userId, std::int32_t t int APS5_VABI sceAudio3dAudioOutClose(std::int32_t handle); int APS5_VABI sceAudio3dAudioOutOutput(std::int32_t handle, const void* ptr); int APS5_VABI sceAudio3dAudioOutOutputs(AudioOutOutputParam* param, uint32_t num); -int APS5_VABI sceAudio3dStrError(void); +const char* APS5_VABI sceAudio3dStrError(std::int32_t error); int APS5_VABI sceAudio3dPortOpenEx(void); int APS5_VABI sceAudio3dPortCreate(void); int APS5_VABI sceAudio3dPortDestroy(void); diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 4459dc8152..cc2610c23e 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -403,20 +403,28 @@ static int allocatePort(int type, std::uint32_t len, std::uint32_t freq, Format } static std::atomic g_audioOut2Initialized{false}; +static std::atomic g_audioOutInitialized{false}; +static constexpr int SCE_AUDIO_OUT2_ERROR_ALREADY_INITIALIZED = static_cast(0x80268004); +static constexpr int SCE_AUDIO_OUT_ERROR_ALREADY_INITIALIZED = static_cast(0x8026000e); bool AudioOut2IsInitialized_nid_no_patch() { return g_audioOut2Initialized.load(); } +void AudioOut2MarkInitialized_nid_no_patch() { + g_audioOut2Initialized.store(true); +} + extern "C" { int APS5_VABI sceAudioOut2Initialize(void) { - g_audioOut2Initialized.store(true); - return 0; + return g_audioOut2Initialized.exchange(true) ? SCE_AUDIO_OUT2_ERROR_ALREADY_INITIALIZED : 0; } int APS5_VABI sceAudioOutInit() { - return sceAudioOut2Initialize(); + if (g_audioOutInitialized.exchange(true)) return SCE_AUDIO_OUT_ERROR_ALREADY_INITIALIZED; + AudioOut2MarkInitialized_nid_no_patch(); + return 0; } int APS5_VABI sceAudioOutOpen(int userId, int type, int index, std::uint32_t len, @@ -663,6 +671,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/AudioOut2Internal.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp index 42363a9998..792d1a525c 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp @@ -26,6 +26,7 @@ bool AudioOut2TraceEnabled(); // Seconds since the first AudioOut2 call, for the trace. double AudioOut2TraceSeconds(); bool AudioOut2IsInitialized_nid_no_patch(); +void AudioOut2MarkInitialized_nid_no_patch(); #define AUDIOOUT2_TRACE(...) \ do { \ diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp index e427f40302..30372ef36f 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp @@ -3,11 +3,13 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "AudioOut2Internal.hpp" extern "C" { int APS5_VABI sceAudioOut2LoInit(void) { - return sceAudioOut2Initialize(); + AudioOut2MarkInitialized_nid_no_patch(); + return 0; } int APS5_VABI sceAudioOut2LoTerminate(void) { diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp index 6723eb1371..0a88e02d41 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp @@ -15,7 +15,8 @@ extern "C" { int APS5_VABI sceAudioOut2InitializeEx(std::uint32_t param) { (void)param; - return sceAudioOut2Initialize(); + AudioOut2MarkInitialized_nid_no_patch(); + return 0; } int APS5_VABI sceAudioOut2Set3DLatency(int userId, std::uint32_t output) { 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/AudioOut2DeviceState.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2DeviceState.cpp index 82e8464648..dffab531e6 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2DeviceState.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2DeviceState.cpp @@ -114,6 +114,12 @@ void SpeakerInfoAfterAudioOutInit() { Require(sceAudioOut2GetSpeakerInfo(&info, 0) == 0); } +void RepeatedInitializationAfterAudioOutInit() { + Require(sceAudioOut2Initialize() == static_cast(0x80268004)); + Require(sceAudioOut2Initialize() == static_cast(0x80268004)); + Require(sceAudioOutInit() == static_cast(0x8026000e)); +} + void SpeakerInfoRejectsBadFlags() { AudioOut2SpeakerInfo info; std::memset(&info, 0xAB, sizeof(info)); @@ -145,6 +151,7 @@ int main() { SpeakerInfoRequiresInitialization(); SpeakerInfoAfterAudioOutInit(); + RepeatedInitializationAfterAudioOutInit(); AudioOut2ContextParam params{}; Require(sceAudioOut2ContextResetParam(¶ms) == 0); diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp index cc08e950f6..6fee544dd5 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp @@ -228,6 +228,10 @@ void TestLowLatencyService() { int main() { Require(sceAudioOut2Initialize() == 0, "initialization must succeed"); + Require(sceAudioOut2Initialize() == static_cast(0x80268004), "a second AudioOut2 initialization fails"); + Require(sceAudioOutInit() == 0, "the first legacy initialization succeeds after AudioOut2's"); + Require(sceAudioOut2Initialize() == static_cast(0x80268004), "AudioOut2 initialization fails after the legacy one"); + Require(sceAudioOutInit() == static_cast(0x8026000e), "a second legacy initialization fails"); TestSet3DLatency(); TestMasteringInit(); TestMasteringTerm(); 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/libSceAudiodec.native/CMakeLists.txt b/core/libs/prx/libSceAudiodec.native/CMakeLists.txt index a4e07a5c01..9820917503 100644 --- a/core/libs/prx/libSceAudiodec.native/CMakeLists.txt +++ b/core/libs/prx/libSceAudiodec.native/CMakeLists.txt @@ -1,2 +1,3 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceAudiodec/AudiodecLibrary.cmake) add_sce_audiodec_library(libSceAudiodec.native) +target_sources(libSceAudiodec.native PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) diff --git a/core/libs/prx/libSceAvPlayer.native/CMakeLists.txt b/core/libs/prx/libSceAvPlayer.native/CMakeLists.txt index 5ba3ecebdb..b9a16defb7 100644 --- a/core/libs/prx/libSceAvPlayer.native/CMakeLists.txt +++ b/core/libs/prx/libSceAvPlayer.native/CMakeLists.txt @@ -1,2 +1,4 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceAvPlayer/AvPlayerLibrary.cmake) add_sce_avplayer_library(libSceAvPlayer.native) +target_sources(libSceAvPlayer.native PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) +set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp PROPERTIES COMPILE_OPTIONS -fvisibility=default) diff --git a/core/libs/prx/libSceCamera/CMakeLists.txt b/core/libs/prx/libSceCamera/CMakeLists.txt index 27f982657d..8591dce44f 100644 --- a/core/libs/prx/libSceCamera/CMakeLists.txt +++ b/core/libs/prx/libSceCamera/CMakeLists.txt @@ -1,4 +1,10 @@ -add_library(libSceCamera SHARED EXCLUDE_FROM_ALL Export.cpp) +add_library(libSceCamera SHARED EXCLUDE_FROM_ALL + ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Handles.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Attributes.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Config.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Device.cpp +) target_include_directories(libSceCamera PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceCamera) @@ -18,4 +24,14 @@ if(BUILD_TESTING) set_tests_properties(guest_camera PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_camera_settings_tests ${LIBS_INCLUDE_DIR}/tests/GuestCameraSettings.cpp) + target_include_directories(guest_camera_settings_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_camera_settings_tests PRIVATE libSceCamera libc) + configure_windows_unwind(guest_camera_settings_tests) + add_test(NAME guest_camera_settings COMMAND guest_camera_settings_tests) + if(WIN32) + set_tests_properties(guest_camera_settings PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceCamera/Export.cpp b/core/libs/prx/libSceCamera/Export.cpp index c4fa002d9d..974e00fa3e 100644 --- a/core/libs/prx/libSceCamera/Export.cpp +++ b/core/libs/prx/libSceCamera/Export.cpp @@ -4,7 +4,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -static constexpr int SCE_CAMERA_ERROR_PARAM = static_cast(0x802E0000); +#include "prx/libSceCamera/include/Camera.hpp" extern "C" { @@ -13,71 +13,11 @@ int APS5_VABI sceCameraIsAttached(int32_t index) { return 0; } -int APS5_VABI sceCameraOpen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraStart(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraStop(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraGetConfig(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraSetConfig(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraSetAttribute(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraGetDeviceInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraGetFrameData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraIsValidFrameData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraStopByHandle(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCameraGetDeviceIDWithoutOpen(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCameraGetAttribute(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCameraInitializeRegistryCalibData(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -113,21 +53,11 @@ int APS5_VABI sceCameraSetTrackerMode(void) { return 0; } -int APS5_VABI sceCameraSetContrast(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCameraGetCalibDataForEve(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCameraGetCalibDataFromDevice(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCamera2SetContrast(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -153,11 +83,6 @@ int APS5_VABI sceCameraSetVideoSyncInternal(void) { return 0; } -int APS5_VABI sceCameraSetConfigInternal(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCameraSetAttributeInternal(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -168,26 +93,11 @@ int APS5_VABI sceCamera2SetWhiteBalance(void) { return 0; } -int APS5_VABI sceCameraGetMmapConnectedCount(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCamera2GetSaturation(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCameraGetDeviceConfig(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraAudioOpen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCamera2GetAutoExposureGain(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -198,11 +108,6 @@ int APS5_VABI sceCameraGetCalibDataFromDeviceForEve(void) { return 0; } -int APS5_VABI sceCameraGetWhiteBalance(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCameraGetVrCaptureInfo(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -218,11 +123,6 @@ int APS5_VABI sceCamera2GetAttribute(void) { return 0; } -int APS5_VABI sceCameraSetLensCorrection(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCameraSetCalibData(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -238,26 +138,11 @@ int APS5_VABI sceCamera2SetConfig(void) { return 0; } -int APS5_VABI sceCameraGetExposureGain(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraSetSharpness(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCamera2Initialize(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCameraSetHue(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCameraAccGetData(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -268,16 +153,6 @@ int APS5_VABI sceCamera2SetAttribute(void) { return 0; } -int APS5_VABI sceCameraGetCalibrationData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraGetSaturation(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCamera2GetHue(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -303,21 +178,6 @@ int APS5_VABI sceCameraAudioClose(void) { return 0; } -int APS5_VABI sceCameraGetProductInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraGetGamma(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraGetLensCorrection(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCameraChangeAppModuleState(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -328,31 +188,11 @@ int APS5_VABI sceCamera2GetFrameData(void) { return 0; } -int APS5_VABI sceCameraGetContrast(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraSetSaturation(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraGetSharpness(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCamera2GetExposureGain(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCameraStartByHandle(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCamera2Open(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -363,11 +203,6 @@ int APS5_VABI sceCamera2Start(void) { return 0; } -int APS5_VABI sceCameraSetLensCorrectionInternal(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCameraOpenByModuleId(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -403,11 +238,6 @@ int APS5_VABI sceCameraWaitForEveReady(void) { return 0; } -int APS5_VABI sceCameraGetRegistryInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCameraAudioGetData2(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -418,11 +248,6 @@ int APS5_VABI sceCameraSetAppModuleFocus(void) { return 0; } -int APS5_VABI sceCameraSetDefectivePixelCancellation(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCameraGetDeviceID(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -433,11 +258,6 @@ int APS5_VABI sceCameraSetForceActivate(void) { return 0; } -int APS5_VABI sceCameraSetWhiteBalance(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCamera2GetFieldOfView(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -453,11 +273,6 @@ int APS5_VABI sceCameraSetProcessFocus(void) { return 0; } -int APS5_VABI sceCameraSetGamma(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCamera2GetConfig(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -468,11 +283,6 @@ int APS5_VABI sceCamera2GetWhiteBalance(void) { return 0; } -int APS5_VABI sceCameraGetDeviceConfigWithoutHandle(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCameraDeviceOpen(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -483,11 +293,6 @@ int APS5_VABI sceCameraSetUacModeInternal(void) { return 0; } -int APS5_VABI sceCameraGetAutoExposureGain(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCameraGetRegister(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -503,16 +308,6 @@ int APS5_VABI sceCameraRemoveAppModuleFocus(void) { return 0; } -int APS5_VABI sceCameraGetAutoWhiteBalance(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraGetDefectivePixelCancellation(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCamera2Close(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -528,11 +323,6 @@ int APS5_VABI sceCameraWaitForLukeReady(void) { return 0; } -int APS5_VABI sceCameraSetDefectivePixelCancellationInternal(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCamera2GetSharpness(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -558,11 +348,6 @@ int APS5_VABI sceCameraIsConfigChangeDone(void) { return 0; } -int APS5_VABI sceCameraSetExposureGain(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCameraSetVideoSync(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -572,24 +357,4 @@ int APS5_VABI sceCameraAudioReset(void) { NotImplemented_nid_no_patch(__func__); return 0; } - -int APS5_VABI sceCameraSetAutoExposureGain(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraSetAutoWhiteBalance(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraGetHue(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCameraCloseByHandle(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} } diff --git a/core/libs/prx/libSceCamera/include/Api.hpp b/core/libs/prx/libSceCamera/include/Api.hpp index 169844eb58..34907d556f 100644 --- a/core/libs/prx/libSceCamera/include/Api.hpp +++ b/core/libs/prx/libSceCamera/include/Api.hpp @@ -2,22 +2,116 @@ #include "SceTypes.hpp" +struct CameraFramePosition { + std::uint32_t x; + std::uint32_t y; + std::uint32_t xSize; + std::uint32_t ySize; +}; + +struct CameraExposureGain { + std::uint32_t exposureControl; + std::uint32_t exposure; + std::uint32_t gain; + std::uint32_t mode; +}; + +struct CameraWhiteBalance { + std::uint32_t whiteBalanceControl; + std::uint32_t gainRed; + std::uint32_t gainBlue; + std::uint32_t gainGreen; +}; + +struct CameraGamma { + std::uint32_t gammaControl; + std::uint32_t value; + std::uint8_t reserved[16]; +}; + +struct CameraAttribute { + std::uint32_t sizeThis; + std::uint32_t channel; + CameraFramePosition framePosition; + CameraExposureGain exposureGain; + CameraWhiteBalance whiteBalance; + CameraGamma gamma; + std::uint32_t saturation; + std::uint32_t contrast; + std::uint32_t sharpness; + std::int32_t hue; + std::uint32_t reserved[4]; +}; +static_assert(sizeof(CameraAttribute) == 112); + +struct CameraAutoExposureGainTarget { + std::uint32_t sizeThis; + std::uint32_t target; +}; + +struct CameraConfigExtension { + std::uint32_t formatLevel[4]; + std::uint32_t resolution; + std::uint32_t framerate; + std::uint32_t width; + std::uint32_t height; + std::uint32_t reserved; + void* baseOption; +}; + +struct CameraConfig { + std::uint32_t sizeThis; + std::uint32_t configType; + CameraConfigExtension extension[2]; +}; +static_assert(sizeof(CameraConfig) == 104); + +struct CameraDeviceInfo { + std::uint32_t sizeThis; + std::uint32_t infoRevision; + std::uint32_t deviceRevision; + std::uint32_t padding; +}; + +struct CameraGetCalibrationDataParameter { + std::uint32_t size; + std::uint32_t formatType; + std::uint32_t functionType; +}; + +struct CameraStartParameter { + std::uint32_t sizeThis; + std::uint32_t formatLevel[2]; + void* startOption; +}; + +struct CameraFrameData { + std::uint32_t sizeThis; + std::uint32_t readMode; + std::uint32_t framePosition[2][4][4]; + void* framePointerList[2][4]; + std::uint32_t frameSize[2][4]; + std::uint32_t status[2]; + std::uint8_t reserved[344]; +}; +static_assert(sizeof(CameraFrameData) == 584); + extern "C" { int APS5_VABI sceCameraIsAttached(int32_t index); -int APS5_VABI sceCameraOpen(void); -int APS5_VABI sceCameraClose(void); -int APS5_VABI sceCameraStart(void); -int APS5_VABI sceCameraStop(void); -int APS5_VABI sceCameraGetConfig(void); -int APS5_VABI sceCameraSetConfig(void); -int APS5_VABI sceCameraSetAttribute(void); -int APS5_VABI sceCameraGetDeviceInfo(void); -int APS5_VABI sceCameraGetFrameData(void); -int APS5_VABI sceCameraIsValidFrameData(void); -int APS5_VABI sceCameraStopByHandle(void); +int APS5_VABI sceCameraOpen(std::int32_t userId, std::int32_t type, std::int32_t index, void*); +int APS5_VABI sceCameraClose(std::int32_t handle); +int APS5_VABI sceCameraStart(std::int32_t handle, const CameraStartParameter* param); +int APS5_VABI sceCameraStop(std::int32_t handle); +int APS5_VABI sceCameraGetConfig(std::int32_t handle, CameraConfig* config); +int APS5_VABI sceCameraSetConfig(std::int32_t handle, const CameraConfig* config); +int APS5_VABI sceCameraSetAttribute(std::int32_t handle, const CameraAttribute* attribute); +int APS5_VABI sceCameraGetDeviceInfo(std::int32_t reserved, const CameraDeviceInfo* deviceInfo); +int APS5_VABI sceCameraGetFrameData(std::int32_t handle, CameraFrameData* frameData); +int APS5_VABI sceCameraIsValidFrameData(std::int32_t handle, const CameraFrameData* frameData); +int APS5_VABI sceCameraStopByHandle(std::int32_t handle); int APS5_VABI sceCameraGetDeviceIDWithoutOpen(void); -int APS5_VABI sceCameraGetAttribute(void); +int APS5_VABI sceCameraGetAttribute(std::int32_t handle, CameraAttribute* attribute); int APS5_VABI sceCameraInitializeRegistryCalibData(void); int APS5_VABI sceCamera2GetContrast(void); int APS5_VABI sceCamera2Finalize(void); @@ -25,7 +119,7 @@ int APS5_VABI sceCameraGetConnectedDeviceAndDriverMode(void); int APS5_VABI sceCamera2IsAttached(void); int APS5_VABI sceCamera2SetSharpness(void); int APS5_VABI sceCameraSetTrackerMode(void); -int APS5_VABI sceCameraSetContrast(void); +int APS5_VABI sceCameraSetContrast(std::int32_t handle, std::uint32_t channel, std::uint32_t, const void* option); int APS5_VABI sceCameraGetCalibDataForEve(void); int APS5_VABI sceCameraGetCalibDataFromDevice(void); int APS5_VABI sceCamera2SetContrast(void); @@ -33,49 +127,49 @@ int APS5_VABI sceCameraIsConnectedForSystem(void); int APS5_VABI sceCamera2SetExposureGain(void); int APS5_VABI sceCameraSetRegister(void); int APS5_VABI sceCameraSetVideoSyncInternal(void); -int APS5_VABI sceCameraSetConfigInternal(void); +int APS5_VABI sceCameraSetConfigInternal(std::int32_t handle, const CameraConfig* config); int APS5_VABI sceCameraSetAttributeInternal(void); int APS5_VABI sceCamera2SetWhiteBalance(void); -int APS5_VABI sceCameraGetMmapConnectedCount(void); +int APS5_VABI sceCameraGetMmapConnectedCount(std::uint32_t* count); int APS5_VABI sceCamera2GetSaturation(void); -int APS5_VABI sceCameraGetDeviceConfig(void); +int APS5_VABI sceCameraGetDeviceConfig(std::int32_t handle, CameraConfig* config); int APS5_VABI sceCameraAudioOpen(void); int APS5_VABI sceCamera2GetAutoExposureGain(void); int APS5_VABI sceCameraGetCalibDataFromDeviceForEve(void); -int APS5_VABI sceCameraGetWhiteBalance(void); +int APS5_VABI sceCameraGetWhiteBalance(std::int32_t handle, std::uint32_t channel, CameraWhiteBalance* whiteBalance, void* option); int APS5_VABI sceCameraGetVrCaptureInfo(void); int APS5_VABI sceCameraSetCalibDataForEve(void); int APS5_VABI sceCamera2GetAttribute(void); -int APS5_VABI sceCameraSetLensCorrection(void); +int APS5_VABI sceCameraSetLensCorrection(std::int32_t handle, std::uint32_t channel, std::uint32_t enable, const void* option); int APS5_VABI sceCameraSetCalibData(void); int APS5_VABI sceCameraSetProcessFocusByHandle(void); int APS5_VABI sceCamera2SetConfig(void); -int APS5_VABI sceCameraGetExposureGain(void); -int APS5_VABI sceCameraSetSharpness(void); +int APS5_VABI sceCameraGetExposureGain(std::int32_t handle, std::uint32_t channel, CameraExposureGain* exposureGain, void* option); +int APS5_VABI sceCameraSetSharpness(std::int32_t handle, std::uint32_t channel, std::uint32_t sharpness, const void* option); int APS5_VABI sceCamera2Initialize(void); -int APS5_VABI sceCameraSetHue(void); +int APS5_VABI sceCameraSetHue(std::int32_t handle, std::uint32_t channel, std::int32_t, const void* option); int APS5_VABI sceCameraAccGetData(void); int APS5_VABI sceCamera2SetAttribute(void); -int APS5_VABI sceCameraGetCalibrationData(void); -int APS5_VABI sceCameraGetSaturation(void); +int APS5_VABI sceCameraGetCalibrationData(const CameraGetCalibrationDataParameter* param, void* calibrationData); +int APS5_VABI sceCameraGetSaturation(std::int32_t handle, std::uint32_t channel, std::uint32_t* saturation, void* option); int APS5_VABI sceCamera2GetHue(void); int APS5_VABI sceCamera2Stop(void); int APS5_VABI sceCamera2SetSaturation(void); int APS5_VABI sceCamera2GetAutoWhiteBalance(void); int APS5_VABI sceCameraAudioClose(void); -int APS5_VABI sceCameraGetProductInfo(void); -int APS5_VABI sceCameraGetGamma(void); -int APS5_VABI sceCameraGetLensCorrection(void); +int APS5_VABI sceCameraGetProductInfo(void* productInfo); +int APS5_VABI sceCameraGetGamma(std::int32_t handle, std::uint32_t channel, CameraGamma* gamma, void* option); +int APS5_VABI sceCameraGetLensCorrection(std::int32_t handle, std::uint32_t channel, std::uint32_t* enable, void* option); int APS5_VABI sceCameraChangeAppModuleState(void); int APS5_VABI sceCamera2GetFrameData(void); -int APS5_VABI sceCameraGetContrast(void); -int APS5_VABI sceCameraSetSaturation(void); -int APS5_VABI sceCameraGetSharpness(void); +int APS5_VABI sceCameraGetContrast(std::int32_t handle, std::uint32_t channel, std::uint32_t* contrast, void* option); +int APS5_VABI sceCameraSetSaturation(std::int32_t handle, std::uint32_t channel, std::uint32_t, const void* option); +int APS5_VABI sceCameraGetSharpness(std::int32_t handle, std::uint32_t channel, std::uint32_t* sharpness, void* option); int APS5_VABI sceCamera2GetExposureGain(void); -int APS5_VABI sceCameraStartByHandle(void); +int APS5_VABI sceCameraStartByHandle(std::int32_t handle, const CameraStartParameter* param); int APS5_VABI sceCamera2Open(void); int APS5_VABI sceCamera2Start(void); -int APS5_VABI sceCameraSetLensCorrectionInternal(void); +int APS5_VABI sceCameraSetLensCorrectionInternal(std::int32_t handle, std::uint32_t channel, std::uint32_t enable, const void* option); int APS5_VABI sceCameraOpenByModuleId(void); int APS5_VABI sceCamera2GetCalibrationData(void); int APS5_VABI sceCameraAudioGetData(void); @@ -83,43 +177,43 @@ int APS5_VABI sceCamera2SetHue(void); int APS5_VABI sceCamera2GetGamma(void); int APS5_VABI sceCameraGetCalibData(void); int APS5_VABI sceCameraWaitForEveReady(void); -int APS5_VABI sceCameraGetRegistryInfo(void); +int APS5_VABI sceCameraGetRegistryInfo(void* registryInfo); int APS5_VABI sceCameraAudioGetData2(void); int APS5_VABI sceCameraSetAppModuleFocus(void); -int APS5_VABI sceCameraSetDefectivePixelCancellation(void); +int APS5_VABI sceCameraSetDefectivePixelCancellation(std::int32_t handle, std::uint32_t channel, std::uint32_t enable, const void* option); int APS5_VABI sceCameraGetDeviceID(void); int APS5_VABI sceCameraSetForceActivate(void); -int APS5_VABI sceCameraSetWhiteBalance(void); +int APS5_VABI sceCameraSetWhiteBalance(std::int32_t handle, std::uint32_t channel, const CameraWhiteBalance* whiteBalance, const void* option); int APS5_VABI sceCamera2GetFieldOfView(void); int APS5_VABI sceCamera2IsValidFrameData(void); int APS5_VABI sceCameraSetProcessFocus(void); -int APS5_VABI sceCameraSetGamma(void); +int APS5_VABI sceCameraSetGamma(std::int32_t handle, std::uint32_t channel, const CameraGamma* gamma, const void* option); int APS5_VABI sceCamera2GetConfig(void); int APS5_VABI sceCamera2GetWhiteBalance(void); -int APS5_VABI sceCameraGetDeviceConfigWithoutHandle(void); +int APS5_VABI sceCameraGetDeviceConfigWithoutHandle(CameraConfig* config); int APS5_VABI sceCameraDeviceOpen(void); int APS5_VABI sceCameraSetUacModeInternal(void); -int APS5_VABI sceCameraGetAutoExposureGain(void); +int APS5_VABI sceCameraGetAutoExposureGain(std::int32_t handle, std::uint32_t channel, std::uint32_t* enable, CameraAutoExposureGainTarget* option); int APS5_VABI sceCameraGetRegister(void); int APS5_VABI sceCamera2SetGamma(void); int APS5_VABI sceCameraRemoveAppModuleFocus(void); -int APS5_VABI sceCameraGetAutoWhiteBalance(void); -int APS5_VABI sceCameraGetDefectivePixelCancellation(void); +int APS5_VABI sceCameraGetAutoWhiteBalance(std::int32_t handle, std::uint32_t channel, std::uint32_t* enable, void* option); +int APS5_VABI sceCameraGetDefectivePixelCancellation(std::int32_t handle, std::uint32_t channel, std::uint32_t* enable, void* option); int APS5_VABI sceCamera2Close(void); int APS5_VABI sceCamera2SetVideoSync(void); int APS5_VABI sceCameraWaitForLukeReady(void); -int APS5_VABI sceCameraSetDefectivePixelCancellationInternal(void); +int APS5_VABI sceCameraSetDefectivePixelCancellationInternal(std::int32_t handle, std::uint32_t channel, std::uint32_t enable, const void* option); int APS5_VABI sceCamera2GetSharpness(void); int APS5_VABI sceCamera2SetAutoWhiteBalance(void); int APS5_VABI sceCameraSetDebugStop(void); int APS5_VABI sceCamera2SetAutoExposureGain(void); int APS5_VABI sceCameraIsConfigChangeDone(void); -int APS5_VABI sceCameraSetExposureGain(void); +int APS5_VABI sceCameraSetExposureGain(std::int32_t handle, std::uint32_t channel, const CameraExposureGain* exposureGain, const void* option); int APS5_VABI sceCameraSetVideoSync(void); int APS5_VABI sceCameraAudioReset(void); -int APS5_VABI sceCameraSetAutoExposureGain(void); -int APS5_VABI sceCameraSetAutoWhiteBalance(void); -int APS5_VABI sceCameraGetHue(void); -int APS5_VABI sceCameraCloseByHandle(void); +int APS5_VABI sceCameraSetAutoExposureGain(std::int32_t handle, std::uint32_t channel, std::uint32_t, const CameraAutoExposureGainTarget* option); +int APS5_VABI sceCameraSetAutoWhiteBalance(std::int32_t handle, std::uint32_t channel, std::uint32_t, const void* option); +int APS5_VABI sceCameraGetHue(std::int32_t handle, std::uint32_t channel, std::int32_t* hue, void* option); +int APS5_VABI sceCameraCloseByHandle(std::int32_t handle); } diff --git a/core/libs/prx/libSceCamera/include/Camera.hpp b/core/libs/prx/libSceCamera/include/Camera.hpp new file mode 100644 index 0000000000..fde323f16e --- /dev/null +++ b/core/libs/prx/libSceCamera/include/Camera.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +inline constexpr int SCE_CAMERA_ERROR_PARAM = static_cast(0x802E0000); +inline constexpr int SCE_CAMERA_ERROR_NOT_INIT = static_cast(0x802E0002); +inline constexpr int SCE_CAMERA_ERROR_NOT_OPEN = static_cast(0x802E0004); +inline constexpr int SCE_CAMERA_ERROR_FORMAT_UNKNOWN = static_cast(0x802E0007); +inline constexpr int SCE_CAMERA_ERROR_NOT_CONNECTED = static_cast(0x802E0010); +inline constexpr int SCE_CAMERA_ERROR_FATAL = static_cast(0x802E00FF); + +namespace Camera { + +bool IsOpen(); +bool IsOpen(std::int32_t handle); + +} diff --git a/core/libs/prx/libSceCamera/src/Attributes.cpp b/core/libs/prx/libSceCamera/src/Attributes.cpp new file mode 100644 index 0000000000..d991bf1a0e --- /dev/null +++ b/core/libs/prx/libSceCamera/src/Attributes.cpp @@ -0,0 +1,184 @@ +#include +#include +#include "prx/libSceCamera/libSceCamera.hpp" +#include "prx/libSceCamera/include/Camera.hpp" + +namespace { + +constexpr std::uint32_t CHANNEL_0 = 1; +constexpr std::uint32_t CHANNEL_BOTH = 3; + +constexpr std::uint32_t DEFAULT_CONTRAST = 32; +constexpr std::uint32_t DEFAULT_SATURATION = 64; +constexpr std::uint32_t DEFAULT_SHARPNESS = 1; +constexpr std::int32_t DEFAULT_HUE = 1; +constexpr std::uint32_t DEFAULT_GAMMA = 4; +constexpr CameraWhiteBalance DEFAULT_WHITE_BALANCE{0, 768, 768, 512}; + +bool Readable(std::uint32_t channel) { return channel >= CHANNEL_0 && channel < CHANNEL_BOTH; } + +bool Writable(std::uint32_t channel) { return channel >= CHANNEL_0 && channel <= CHANNEL_BOTH; } + +int CheckGet(std::int32_t handle, std::uint32_t channel, const void* out, const void* option) { + if (handle < 1 || !Readable(channel) || !out || option) return SCE_CAMERA_ERROR_PARAM; + if (!Camera::IsOpen(handle)) return SCE_CAMERA_ERROR_NOT_OPEN; + return 0; +} + +int RefuseSet(std::int32_t handle, std::uint32_t channel, bool valid) { + if (handle < 1 || !Writable(channel) || !valid) return SCE_CAMERA_ERROR_PARAM; + if (!Camera::IsOpen(handle)) return SCE_CAMERA_ERROR_NOT_OPEN; + return SCE_CAMERA_ERROR_NOT_CONNECTED; +} + +bool ValidTarget(const CameraAutoExposureGainTarget* option) { + if (!option) return true; + if (option->sizeThis != sizeof(CameraAutoExposureGainTarget)) return false; + switch (option->target) { + case 0x0: case 0x2: case 0x4: case 0x6: case 0x8: + case 0x10: case 0x12: case 0x14: case 0x16: + case 0x20: + return true; + default: + return false; + } +} + +template +int Report(std::int32_t handle, std::uint32_t channel, TValue* out, const void* option, TValue value) { + if (const int error = CheckGet(handle, channel, out, option)) return error; + *out = value; + return 0; +} + +} + +extern "C" { + +int APS5_VABI sceCameraGetAttribute(std::int32_t handle, CameraAttribute* attribute) { + if (handle < 1 || !attribute || attribute->sizeThis != sizeof(CameraAttribute) || !Readable(attribute->channel)) return SCE_CAMERA_ERROR_PARAM; + if (!Camera::IsOpen(handle)) return SCE_CAMERA_ERROR_NOT_OPEN; + const std::uint32_t channel = attribute->channel; + *attribute = {}; + attribute->sizeThis = sizeof(CameraAttribute); + attribute->channel = channel; + attribute->exposureGain.exposure = 83; + attribute->exposureGain.gain = 100; + attribute->whiteBalance = DEFAULT_WHITE_BALANCE; + attribute->gamma.value = DEFAULT_GAMMA; + attribute->saturation = DEFAULT_SATURATION; + attribute->contrast = DEFAULT_CONTRAST; + attribute->sharpness = DEFAULT_SHARPNESS; + attribute->hue = DEFAULT_HUE; + return 0; +} + +int APS5_VABI sceCameraGetAutoExposureGain(std::int32_t handle, std::uint32_t channel, std::uint32_t* enable, CameraAutoExposureGainTarget* option) { + if (handle < 1 || !Readable(channel) || !enable) return SCE_CAMERA_ERROR_PARAM; + if (option && option->sizeThis != sizeof(CameraAutoExposureGainTarget)) return SCE_CAMERA_ERROR_PARAM; + if (!Camera::IsOpen(handle)) return SCE_CAMERA_ERROR_NOT_OPEN; + *enable = 0; + if (option) *option = {}; + return 0; +} + +int APS5_VABI sceCameraGetAutoWhiteBalance(std::int32_t handle, std::uint32_t channel, std::uint32_t* enable, void* option) { + return Report(handle, channel, enable, option, 0u); +} + +int APS5_VABI sceCameraGetDefectivePixelCancellation(std::int32_t handle, std::uint32_t channel, std::uint32_t* enable, void* option) { + return Report(handle, channel, enable, option, 0u); +} + +int APS5_VABI sceCameraGetLensCorrection(std::int32_t handle, std::uint32_t channel, std::uint32_t* enable, void* option) { + return Report(handle, channel, enable, option, 0u); +} + +int APS5_VABI sceCameraGetContrast(std::int32_t handle, std::uint32_t channel, std::uint32_t* contrast, void* option) { + return Report(handle, channel, contrast, option, DEFAULT_CONTRAST); +} + +int APS5_VABI sceCameraGetSaturation(std::int32_t handle, std::uint32_t channel, std::uint32_t* saturation, void* option) { + return Report(handle, channel, saturation, option, DEFAULT_SATURATION); +} + +int APS5_VABI sceCameraGetSharpness(std::int32_t handle, std::uint32_t channel, std::uint32_t* sharpness, void* option) { + return Report(handle, channel, sharpness, option, DEFAULT_SHARPNESS); +} + +int APS5_VABI sceCameraGetHue(std::int32_t handle, std::uint32_t channel, std::int32_t* hue, void* option) { + return Report(handle, channel, hue, option, DEFAULT_HUE); +} + +int APS5_VABI sceCameraGetExposureGain(std::int32_t handle, std::uint32_t channel, CameraExposureGain* exposureGain, void* option) { + return Report(handle, channel, exposureGain, option, CameraExposureGain{0, 20, 100, 0}); +} + +int APS5_VABI sceCameraGetWhiteBalance(std::int32_t handle, std::uint32_t channel, CameraWhiteBalance* whiteBalance, void* option) { + return Report(handle, channel, whiteBalance, option, DEFAULT_WHITE_BALANCE); +} + +int APS5_VABI sceCameraGetGamma(std::int32_t handle, std::uint32_t channel, CameraGamma* gamma, void* option) { + CameraGamma value{}; + value.value = DEFAULT_GAMMA; + return Report(handle, channel, gamma, option, value); +} + +int APS5_VABI sceCameraSetAttribute(std::int32_t handle, const CameraAttribute* attribute) { + if (!attribute || attribute->sizeThis != sizeof(CameraAttribute)) return SCE_CAMERA_ERROR_PARAM; + return RefuseSet(handle, attribute->channel, true); +} + +int APS5_VABI sceCameraSetAutoExposureGain(std::int32_t handle, std::uint32_t channel, std::uint32_t, const CameraAutoExposureGainTarget* option) { + return RefuseSet(handle, channel, ValidTarget(option)); +} + +int APS5_VABI sceCameraSetAutoWhiteBalance(std::int32_t handle, std::uint32_t channel, std::uint32_t, const void* option) { + return RefuseSet(handle, channel, !option); +} + +int APS5_VABI sceCameraSetDefectivePixelCancellation(std::int32_t handle, std::uint32_t channel, std::uint32_t enable, const void* option) { + return RefuseSet(handle, channel, enable <= 1 && !option); +} + +int APS5_VABI sceCameraSetDefectivePixelCancellationInternal(std::int32_t handle, std::uint32_t channel, std::uint32_t enable, const void* option) { + return RefuseSet(handle, channel, enable <= 2 && !option); +} + +int APS5_VABI sceCameraSetLensCorrection(std::int32_t handle, std::uint32_t channel, std::uint32_t enable, const void* option) { + return RefuseSet(handle, channel, enable <= 1 && !option); +} + +int APS5_VABI sceCameraSetLensCorrectionInternal(std::int32_t handle, std::uint32_t channel, std::uint32_t enable, const void* option) { + return RefuseSet(handle, channel, enable <= 2 && !option); +} + +int APS5_VABI sceCameraSetContrast(std::int32_t handle, std::uint32_t channel, std::uint32_t, const void* option) { + return RefuseSet(handle, channel, !option); +} + +int APS5_VABI sceCameraSetSaturation(std::int32_t handle, std::uint32_t channel, std::uint32_t, const void* option) { + return RefuseSet(handle, channel, !option); +} + +int APS5_VABI sceCameraSetSharpness(std::int32_t handle, std::uint32_t channel, std::uint32_t sharpness, const void* option) { + return RefuseSet(handle, channel, sharpness <= 10 && !option); +} + +int APS5_VABI sceCameraSetHue(std::int32_t handle, std::uint32_t channel, std::int32_t, const void* option) { + return RefuseSet(handle, channel, !option); +} + +int APS5_VABI sceCameraSetExposureGain(std::int32_t handle, std::uint32_t channel, const CameraExposureGain* exposureGain, const void* option) { + return RefuseSet(handle, channel, exposureGain && !option); +} + +int APS5_VABI sceCameraSetWhiteBalance(std::int32_t handle, std::uint32_t channel, const CameraWhiteBalance* whiteBalance, const void* option) { + return RefuseSet(handle, channel, whiteBalance && !option); +} + +int APS5_VABI sceCameraSetGamma(std::int32_t handle, std::uint32_t channel, const CameraGamma* gamma, const void* option) { + return RefuseSet(handle, channel, gamma && !option); +} + +} diff --git a/core/libs/prx/libSceCamera/src/Config.cpp b/core/libs/prx/libSceCamera/src/Config.cpp new file mode 100644 index 0000000000..99270a1341 --- /dev/null +++ b/core/libs/prx/libSceCamera/src/Config.cpp @@ -0,0 +1,59 @@ +#include +#include "prx/libSceCamera/libSceCamera.hpp" +#include "prx/libSceCamera/include/Camera.hpp" + +namespace { + +constexpr std::uint32_t CONFIG_TYPE1 = 0x01; +constexpr std::uint32_t FORMAT_YUV422 = 0x0; +constexpr std::uint32_t SCALE_FORMAT_Y8 = 0x4; +constexpr std::uint32_t RESOLUTION_1280X800 = 0x0; +constexpr std::uint32_t FRAMERATE_60 = 60; + +bool ValidConfig(const CameraConfig* config) { + return config && config->sizeThis == sizeof(CameraConfig); +} + +int CheckConfig(std::int32_t handle, const CameraConfig* config) { + if (handle < 1 || !ValidConfig(config)) return SCE_CAMERA_ERROR_PARAM; + if (!Camera::IsOpen(handle)) return SCE_CAMERA_ERROR_NOT_OPEN; + return 0; +} + +} + +extern "C" { + +int APS5_VABI sceCameraGetConfig(std::int32_t handle, CameraConfig* config) { + if (const int error = CheckConfig(handle, config)) return error; + config->configType = CONFIG_TYPE1; + const CameraConfigExtension extension{{FORMAT_YUV422, SCALE_FORMAT_Y8, SCALE_FORMAT_Y8, SCALE_FORMAT_Y8}, RESOLUTION_1280X800, FRAMERATE_60, 0, 0, 0, nullptr}; + config->extension[0] = extension; + config->extension[1] = extension; + return 0; +} + +int APS5_VABI sceCameraSetConfig(std::int32_t handle, const CameraConfig* config) { + if (const int error = CheckConfig(handle, config)) return error; + return SCE_CAMERA_ERROR_NOT_CONNECTED; +} + +int APS5_VABI sceCameraSetConfigInternal(std::int32_t handle, const CameraConfig* config) { + return sceCameraSetConfig(handle, config); +} + +int APS5_VABI sceCameraGetDeviceConfig(std::int32_t handle, CameraConfig* config) { + if (const int error = CheckConfig(handle, config)) return error; + *config = {}; + config->sizeThis = sizeof(CameraConfig); + return 0; +} + +int APS5_VABI sceCameraGetDeviceConfigWithoutHandle(CameraConfig* config) { + if (!ValidConfig(config)) return SCE_CAMERA_ERROR_PARAM; + if (!Camera::IsOpen()) return SCE_CAMERA_ERROR_NOT_OPEN; + *config = {}; + return 0; +} + +} diff --git a/core/libs/prx/libSceCamera/src/Device.cpp b/core/libs/prx/libSceCamera/src/Device.cpp new file mode 100644 index 0000000000..4387a61fd1 --- /dev/null +++ b/core/libs/prx/libSceCamera/src/Device.cpp @@ -0,0 +1,54 @@ +#include +#include "prx/libSceCamera/libSceCamera.hpp" +#include "prx/libSceCamera/include/Camera.hpp" + +namespace { + +constexpr std::uint32_t CALIBRATION_IMAGE_INVERSE_RECTIFICATION = 1; + +int RefuseQuery(const void* out) { + if (!out) return SCE_CAMERA_ERROR_PARAM; + if (!Camera::IsOpen()) return SCE_CAMERA_ERROR_NOT_INIT; + return SCE_CAMERA_ERROR_NOT_CONNECTED; +} + +} + +extern "C" { + +int APS5_VABI sceCameraGetDeviceInfo(std::int32_t reserved, const CameraDeviceInfo* deviceInfo) { + if (reserved != 0 || !deviceInfo || deviceInfo->sizeThis != sizeof(CameraDeviceInfo) || deviceInfo->infoRevision != 1) return SCE_CAMERA_ERROR_PARAM; + return RefuseQuery(deviceInfo); +} + +int APS5_VABI sceCameraGetProductInfo(void* productInfo) { + return RefuseQuery(productInfo); +} + +int APS5_VABI sceCameraGetRegistryInfo(void* registryInfo) { + return RefuseQuery(registryInfo); +} + +int APS5_VABI sceCameraGetMmapConnectedCount(std::uint32_t* count) { + if (!Camera::IsOpen()) return SCE_CAMERA_ERROR_NOT_OPEN; + if (!count) return SCE_CAMERA_ERROR_PARAM; + *count = 0; + return 0; +} + +int APS5_VABI sceCameraGetCalibDataFromDevice(void) { + return SCE_CAMERA_ERROR_NOT_CONNECTED; +} + +int APS5_VABI sceCameraGetCalibrationData(const CameraGetCalibrationDataParameter* param, void* calibrationData) { + if (!param || !calibrationData || param->size != sizeof(CameraGetCalibrationDataParameter) || param->formatType != 0 || + param->functionType > CALIBRATION_IMAGE_INVERSE_RECTIFICATION) + return SCE_CAMERA_ERROR_PARAM; + return SCE_CAMERA_ERROR_NOT_CONNECTED; +} + +int APS5_VABI sceCameraAudioOpen(void) { + return SCE_CAMERA_ERROR_FATAL; +} + +} diff --git a/core/libs/prx/libSceCamera/src/Handles.cpp b/core/libs/prx/libSceCamera/src/Handles.cpp new file mode 100644 index 0000000000..d6d6100ab3 --- /dev/null +++ b/core/libs/prx/libSceCamera/src/Handles.cpp @@ -0,0 +1,94 @@ +#include +#include +#include +#include +#include +#include "prx/libSceCamera/libSceCamera.hpp" +#include "prx/libSceCamera/include/Camera.hpp" + +namespace { + +constexpr std::int32_t SCE_USER_SERVICE_USER_ID_SYSTEM = 0xFF; +constexpr std::uint32_t MAX_FRAME_DATA_SIZE = 584; + +std::mutex handlesMutex; +std::unordered_set openHandles; +std::int64_t nextHandle = 1; + +int CheckStart(std::int32_t handle, const CameraStartParameter* param) { + if (handle < 1 || !param || param->sizeThis != sizeof(CameraStartParameter)) return SCE_CAMERA_ERROR_PARAM; + if (!Camera::IsOpen(handle)) return SCE_CAMERA_ERROR_NOT_OPEN; + return 0; +} + +} + +bool Camera::IsOpen() { + std::lock_guard lock(handlesMutex); + return !openHandles.empty(); +} + +bool Camera::IsOpen(std::int32_t handle) { + std::lock_guard lock(handlesMutex); + return openHandles.contains(handle); +} + +extern "C" { + +int APS5_VABI sceCameraOpen(std::int32_t userId, std::int32_t type, std::int32_t index, void*) { + if (userId != SCE_USER_SERVICE_USER_ID_SYSTEM || type != 0 || index != 0) return SCE_CAMERA_ERROR_PARAM; + std::lock_guard lock(handlesMutex); + if (nextHandle > std::numeric_limits::max()) throw std::runtime_error("camera handle space exhausted"); + const auto handle = static_cast(nextHandle); + openHandles.insert(handle); + ++nextHandle; + return handle; +} + +int APS5_VABI sceCameraClose(std::int32_t handle) { + if (handle < 1) return SCE_CAMERA_ERROR_PARAM; + std::lock_guard lock(handlesMutex); + if (openHandles.erase(handle) == 0) return SCE_CAMERA_ERROR_NOT_OPEN; + return 0; +} + +int APS5_VABI sceCameraCloseByHandle(std::int32_t handle) { + return sceCameraClose(handle); +} + +int APS5_VABI sceCameraStart(std::int32_t handle, const CameraStartParameter* param) { + if (const int error = CheckStart(handle, param)) return error; + if (param->formatLevel[0] > 0xF || param->formatLevel[1] > 0xF || (param->formatLevel[0] | param->formatLevel[1]) == 0) + return SCE_CAMERA_ERROR_FORMAT_UNKNOWN; + return SCE_CAMERA_ERROR_NOT_CONNECTED; +} + +int APS5_VABI sceCameraStartByHandle(std::int32_t handle, const CameraStartParameter* param) { + return CheckStart(handle, param); +} + +int APS5_VABI sceCameraStop(std::int32_t handle) { + if (handle < 1) return SCE_CAMERA_ERROR_PARAM; + if (!Camera::IsOpen(handle)) return SCE_CAMERA_ERROR_NOT_OPEN; + return 0; +} + +int APS5_VABI sceCameraStopByHandle(std::int32_t handle) { + return sceCameraStop(handle); +} + +int APS5_VABI sceCameraGetFrameData(std::int32_t handle, CameraFrameData* frameData) { + if (!frameData) return SCE_CAMERA_ERROR_PARAM; + frameData->status[0] = UINT32_MAX; + frameData->status[1] = UINT32_MAX; + if (handle < 1 || frameData->sizeThis > MAX_FRAME_DATA_SIZE) return SCE_CAMERA_ERROR_PARAM; + return SCE_CAMERA_ERROR_NOT_OPEN; +} + +int APS5_VABI sceCameraIsValidFrameData(std::int32_t handle, const CameraFrameData* frameData) { + if (handle < 1 || !frameData || frameData->sizeThis > MAX_FRAME_DATA_SIZE) return SCE_CAMERA_ERROR_PARAM; + if (!Camera::IsOpen(handle)) return SCE_CAMERA_ERROR_NOT_OPEN; + return frameData->status[0] == 1 && frameData->status[1] == 1; +} + +} diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 15e18e9e3b..8e7039b5de 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -210,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; @@ -223,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'}; @@ -256,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) { @@ -269,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; } @@ -372,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; @@ -476,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; @@ -501,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; @@ -518,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; @@ -633,27 +664,28 @@ int decodeEucJp(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint 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; + 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 CES_ERROR_SRC_BUFFER_END; - if (mbcs[1] < 0xA1 || mbcs[1] > 0xFE) return CES_ERROR_INVALID_ENCODE; + 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), (lead - 0xA1) * 94 + (mbcs[1] - 0xA1)); + 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 (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; + 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 CES_ERROR_INVALID_ENCODE; + return rejectLead(length); } int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t& length, uint32_t& code) { @@ -669,9 +701,8 @@ int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint3 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) { @@ -680,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; @@ -696,22 +727,22 @@ 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); } return decodeEucJp(profile, mbcs, mbcsMax, length, code); } @@ -722,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}); } @@ -737,13 +771,11 @@ std::vector buildJisReverse(bool strict) { return entries; } -const std::vector& jisReverse(bool strict) { - if (strict) { - static const auto strictReverse = buildJisReverse(true); - return strictReverse; - } - static const auto reverse = buildJisReverse(false); - return reverse; +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) { @@ -772,8 +804,12 @@ 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; @@ -790,8 +826,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; - 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]); @@ -818,8 +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; - 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) { @@ -894,6 +928,15 @@ int convertString(const void* context, const Source* source, uint32_t sourceMax, 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) { if (!accepted) return CES_ERROR_INVALID_PARAMETER; if (!mbcs) return CES_ERROR_INVALID_SRC_BUFFER; @@ -901,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; @@ -959,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; @@ -972,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; @@ -986,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) { @@ -1004,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; @@ -1200,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); } diff --git a/core/libs/prx/libSceCesCs/PendingExports.cpp b/core/libs/prx/libSceCesCs/PendingExports.cpp index ac98b0d50a..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; 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 de9d5746e4..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); @@ -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); 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 9d7e177cb1..dfa37bf121 100644 --- a/core/libs/prx/libSceFont/CMakeLists.txt +++ b/core/libs/prx/libSceFont/CMakeLists.txt @@ -52,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}) @@ -65,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/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/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/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 a729f8c503..0b6823f3b3 100644 --- a/core/libs/prx/libSceFont/src/Text.cpp +++ b/core/libs/prx/libSceFont/src/Text.cpp @@ -64,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; @@ -282,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]; @@ -293,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; @@ -684,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; @@ -692,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 44417ee184..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,8 +97,7 @@ int APS5_VABI sceFontDestroyGraphicsService(FontGraphicsService** service) { } int APS5_VABI sceFontDestroyWords(FontWords** words) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::DestroyWords(words); } @@ -147,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) { @@ -192,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) { @@ -371,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/libSceFreeType/Export.cpp b/core/libs/prx/libSceFreeType/Export.cpp index bb29de9253..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,14 +34,17 @@ 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; } @@ -54,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(); } @@ -164,6 +168,60 @@ int APS5_VABI FT_Done_Face_nid_postfix(GuestFaceRec* face) { 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); diff --git a/core/libs/prx/libSceFreeType/include/Api.hpp b/core/libs/prx/libSceFreeType/include/Api.hpp index edf90a2637..926be2864a 100644 --- a/core/libs/prx/libSceFreeType/include/Api.hpp +++ b/core/libs/prx/libSceFreeType/include/Api.hpp @@ -6,6 +6,9 @@ extern "C" { +std::int32_t APS5_VABI FT_Outline_GetInsideBorder_nid_postfix(const FreeType::GuestOutline* outline); +std::int32_t APS5_VABI FT_Outline_GetOutsideBorder_nid_postfix(const FreeType::GuestOutline* outline); + int APS5_VABI FT_Init_FreeType_nid_postfix(FreeType::GuestLibrary** alibrary); int APS5_VABI FT_Done_FreeType_nid_postfix(FreeType::GuestLibrary* library); int APS5_VABI FT_New_Face_nid_postfix(FreeType::GuestLibrary* library, const char* filepathname, std::int64_t faceIndex, FreeType::GuestFaceRec** aface); @@ -13,10 +16,16 @@ int APS5_VABI FT_New_Memory_Face_nid_postfix(FreeType::GuestLibrary* library, co 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); diff --git a/core/libs/prx/libSceFreeType/include/GuestObjects.hpp b/core/libs/prx/libSceFreeType/include/GuestObjects.hpp index 081d70665f..d47c3c1037 100644 --- a/core/libs/prx/libSceFreeType/include/GuestObjects.hpp +++ b/core/libs/prx/libSceFreeType/include/GuestObjects.hpp @@ -21,6 +21,11 @@ struct GuestLibrary { void UnregisterBitmap_nid_no_patch(const GuestBitmap* bitmap); +struct GuestSize { + GuestSizeRec rec{}; + FT_Size host = nullptr; +}; + struct GuestFace { GuestFaceRec rec{}; GuestGlyphSlotRec slot{}; @@ -28,6 +33,9 @@ struct GuestFace { 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; @@ -43,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/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/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/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/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..15e7dd4e2f 100644 --- a/core/libs/prx/libSceHttp2/CMakeLists.txt +++ b/core/libs/prx/libSceHttp2/CMakeLists.txt @@ -1,6 +1,33 @@ -add_library(libSceHttp2 SHARED EXCLUDE_FROM_ALL Export.cpp) +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 Cookies.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,23 @@ 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_cookie_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttp2Cookies.cpp) + target_include_directories(guest_http2_cookie_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_http2_cookie_tests PRIVATE libSceHttp2 libkernel libc) + configure_windows_unwind(guest_http2_cookie_tests) + add_test(NAME guest_http2_cookies COMMAND guest_http2_cookie_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_cookies 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/Cookies.cpp b/core/libs/prx/libSceHttp2/Cookies.cpp new file mode 100644 index 0000000000..9e767f997a --- /dev/null +++ b/core/libs/prx/libSceHttp2/Cookies.cpp @@ -0,0 +1,429 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "prx/libSceHttp2/include/Http2.hpp" +#include "SceTypes.hpp" +#include "Cookies.hpp" + +namespace { + +constexpr int ERROR_NOT_FOUND = static_cast(0x817B1025); +constexpr int ERROR_INVALID_ID = static_cast(0x817B1100); +constexpr int ERROR_OUT_OF_SIZE = static_cast(0x817B1104); +constexpr int ERROR_INVALID_VALUE = static_cast(0x817B11FE); +constexpr int ERROR_REJECTED = static_cast(0x817B1219); +constexpr int ERROR_INVALID_URL = static_cast(0x817B3060); + +constexpr std::uint32_t DEFAULT_MAX_SIZE = 4096; +constexpr std::uint32_t DEFAULT_MAX_NUM_PER_DOMAIN = 50; +constexpr std::uint32_t DEFAULT_MAX_NUM = 3000; +constexpr std::size_t EVICTED_OVER_MAX_NUM = 10; +constexpr std::size_t HEADER_SLACK = 3; + +constexpr std::int64_t NEVER = std::numeric_limits::max(); +constexpr std::int64_t EARLIEST = std::numeric_limits::min(); + +struct Cookie { + std::string name; + std::string value; + std::string domain; + std::string path; + std::int64_t expiry; + bool hasValue; + bool secure; +}; + +struct CookieBox { + std::vector cookies; + std::uint32_t maxNum = DEFAULT_MAX_NUM; + std::uint32_t maxNumPerDomain = DEFAULT_MAX_NUM_PER_DOMAIN; + std::uint32_t maxSize = DEFAULT_MAX_SIZE; + std::size_t maxInuseSize = 0; + std::uint32_t maxInuseNum = 0; + std::uint32_t removedNum = 0; +}; + +struct Url { + bool secure; + std::string host; + std::string path; +}; + +std::mutex g_cookiesMutex; +std::unordered_map g_cookieBoxes; + +std::int64_t Now() { + return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); +} + +char Lower(char c) { + return c >= 'A' && c <= 'Z' ? static_cast(c - 'A' + 'a') : c; +} + +std::string LowerCopy(std::string_view text) { + std::string lowered(text); + std::transform(lowered.begin(), lowered.end(), lowered.begin(), Lower); + return lowered; +} + +bool EqualsIgnoreCase(std::string_view left, std::string_view right) { + return left.size() == right.size() && std::equal(left.begin(), left.end(), right.begin(), [](char a, char b) { return Lower(a) == Lower(b); }); +} + +std::string_view Trim(std::string_view text) { + while (!text.empty() && (text.front() == ' ' || text.front() == '\t')) text.remove_prefix(1); + while (!text.empty() && (text.back() == ' ' || text.back() == '\t')) text.remove_suffix(1); + return text; +} + +int ParseUrl(std::string_view text, Url& url) { + const std::size_t separator = text.find("://"); + url.secure = separator != std::string_view::npos && EqualsIgnoreCase(text.substr(0, separator), "https"); + std::string_view rest = separator == std::string_view::npos ? text : text.substr(separator + 3); + const std::size_t authorityEnd = std::min(rest.find_first_of("/?#"), rest.size()); + std::string_view authority = rest.substr(0, authorityEnd); + if (const std::size_t at = authority.rfind('@'); at != std::string_view::npos) authority.remove_prefix(at + 1); + if (!authority.empty() && authority.front() == '[') { + const std::size_t close = authority.find(']'); + if (close == std::string_view::npos) return ERROR_INVALID_URL; + authority = authority.substr(0, close + 1); + } else if (const std::size_t colon = authority.find(':'); colon != std::string_view::npos) { + authority = authority.substr(0, colon); + } + if (authority.empty()) return ERROR_REJECTED; + url.host = LowerCopy(authority); + std::string_view path = rest.substr(authorityEnd); + path = path.substr(0, std::min(path.find_first_of("?#"), path.size())); + url.path = path.empty() || path.front() != '/' ? "/" : std::string(path); + return 0; +} + +std::string DefaultPath(const std::string& path) { + const std::size_t slash = path.rfind('/'); + return slash == std::string::npos ? "/" : path.substr(0, slash + 1); +} + +bool IsIpAddress(const std::string& host) { + return host.front() == '[' || host.find_first_not_of("0123456789.") == std::string::npos; +} + +bool DomainMatches(const std::string& host, const std::string& domain) { + if (host == domain) return true; + return host.size() > domain.size() && host.compare(host.size() - domain.size(), domain.size(), domain) == 0 && host[host.size() - domain.size() - 1] == '.' && !IsIpAddress(host); +} + +bool PathMatches(const std::string& requestPath, const std::string& cookiePath) { + return requestPath.compare(0, cookiePath.size(), cookiePath) == 0; +} + +bool IsDateDelimiter(char c) { + const auto u = static_cast(c); + return u == 0x09 || (u >= 0x20 && u <= 0x2F) || (u >= 0x3B && u <= 0x40) || (u >= 0x5B && u <= 0x60) || (u >= 0x7B && u <= 0x7E); +} + +bool IsDigit(char c) { + return c >= '0' && c <= '9'; +} + +bool LeadingDigits(std::string_view token, std::size_t minimum, std::size_t maximum, int& value, std::size_t& used) { + used = 0; + value = 0; + while (used < token.size() && IsDigit(token[used])) value = value * 10 + (token[used++] - '0'); + return used >= minimum && used <= maximum; +} + +bool ParseTime(std::string_view token, int& hour, int& minute, int& second) { + int* fields[3] = {&hour, &minute, &second}; + for (int i = 0; i < 3; ++i) { + std::size_t used = 0; + if (!LeadingDigits(token, 1, 2, *fields[i], used)) return false; + token.remove_prefix(used); + if (i < 2) { + if (token.empty() || token.front() != ':') return false; + token.remove_prefix(1); + } + } + return true; +} + +std::int64_t DaysFromCivil(int year, int month, int day) { + year -= month <= 2; + const std::int64_t era = (year >= 0 ? year : year - 399) / 400; + const std::int64_t yearOfEra = year - era * 400; + const std::int64_t dayOfYear = (153 * (month + (month > 2 ? -3 : 9)) + 2) / 5 + day - 1; + const std::int64_t dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear; + return era * 146097 + dayOfEra - 719468; +} + +std::optional ParseCookieDate(std::string_view text) { + static constexpr const char* MONTHS[12] = {"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"}; + bool foundTime = false; + bool foundDay = false; + bool foundMonth = false; + bool foundYear = false; + int hour = 0, minute = 0, second = 0, day = 0, month = 0, year = 0; + std::size_t position = 0; + while (position < text.size()) { + while (position < text.size() && IsDateDelimiter(text[position])) ++position; + const std::size_t start = position; + while (position < text.size() && !IsDateDelimiter(text[position])) ++position; + const std::string_view token = text.substr(start, position - start); + if (token.empty()) continue; + int value = 0; + std::size_t used = 0; + if (!foundTime && ParseTime(token, hour, minute, second)) { + foundTime = true; + } else if (!foundDay && LeadingDigits(token, 1, 2, value, used) && (used == token.size() || !IsDigit(token[used]))) { + foundDay = true; + day = value; + } else if (const auto* name = token.size() >= 3 ? std::find_if(std::begin(MONTHS), std::end(MONTHS), [&](const char* candidate) { return EqualsIgnoreCase(token.substr(0, 3), candidate); }) : std::end(MONTHS); + !foundMonth && name != std::end(MONTHS)) { + foundMonth = true; + month = 1 + static_cast(name - std::begin(MONTHS)); + } else if (!foundYear && LeadingDigits(token, 2, 4, value, used) && (used == token.size() || !IsDigit(token[used]))) { + foundYear = true; + year = value; + } + } + if (foundYear && year >= 70 && year <= 99) year += 1900; + if (foundYear && year >= 0 && year <= 69) year += 2000; + if (!foundTime || !foundDay || !foundMonth || !foundYear) return std::nullopt; + if (day < 1 || day > 31 || year < 1601 || hour > 23 || minute > 59 || second > 59) return std::nullopt; + const std::int64_t monthStart = DaysFromCivil(year, month, 1); + const std::int64_t nextMonthStart = month < 12 ? DaysFromCivil(year, month + 1, 1) : DaysFromCivil(year + 1, 1, 1); + if (day > nextMonthStart - monthStart) return std::nullopt; + return DaysFromCivil(year, month, day) * 86400 + hour * 3600 + minute * 60 + second; +} + +std::optional ParseMaxAge(std::string_view text, std::int64_t now) { + const bool negative = !text.empty() && text.front() == '-'; + if (negative) text.remove_prefix(1); + if (text.empty() || !std::all_of(text.begin(), text.end(), IsDigit)) return std::nullopt; + if (negative) return EARLIEST; + std::int64_t seconds = 0; + for (const char c : text) { + if (seconds > (NEVER - (c - '0')) / 10) return NEVER; + seconds = seconds * 10 + (c - '0'); + } + if (seconds == 0) return EARLIEST; + return seconds > NEVER - now ? NEVER : now + seconds; +} + +std::size_t PairSize(const Cookie& cookie) { + return cookie.name.size() + cookie.value.size(); +} + +std::size_t StoredSize(const Cookie& cookie) { + return PairSize(cookie) + cookie.domain.size() + cookie.path.size() + 5; +} + +std::size_t InuseSize(const CookieBox& box) { + std::size_t size = 0; + for (const Cookie& cookie : box.cookies) size += StoredSize(cookie); + return size; +} + +void RemoveExpired(CookieBox& box, std::int64_t now) { + box.cookies.erase(std::remove_if(box.cookies.begin(), box.cookies.end(), [&](const Cookie& cookie) { return cookie.expiry <= now; }), box.cookies.end()); +} + +void MakeRoom(CookieBox& box, const std::string& domain) { + const auto inDomain = [&](const Cookie& cookie) { return cookie.domain == domain; }; + while (true) { + const auto count = static_cast(std::count_if(box.cookies.begin(), box.cookies.end(), inDomain)); + if (count == 0 || count < box.maxNumPerDomain) break; + box.cookies.erase(std::find_if(box.cookies.begin(), box.cookies.end(), inDomain)); + ++box.removedNum; + } + if (box.cookies.size() >= box.maxNum) { + const std::size_t evicted = std::min(box.cookies.size(), EVICTED_OVER_MAX_NUM); + box.cookies.erase(box.cookies.begin(), box.cookies.begin() + static_cast(evicted)); + box.removedNum += static_cast(evicted); + } +} + +void RecordUsage(CookieBox& box) { + box.maxInuseSize = std::max(box.maxInuseSize, InuseSize(box)); + box.maxInuseNum = std::max(box.maxInuseNum, static_cast(box.cookies.size())); +} + +int StoreCookie(CookieBox& box, const Url& url, std::string_view text, std::int64_t now) { + const std::size_t pairEnd = std::min(text.find(';'), text.size()); + const std::string_view pair = text.substr(0, pairEnd); + const std::size_t equals = pair.find('='); + Cookie cookie{}; + cookie.name = std::string(Trim(pair.substr(0, equals))); + cookie.hasValue = equals != std::string_view::npos; + if (cookie.hasValue) cookie.value = std::string(Trim(pair.substr(equals + 1))); + cookie.expiry = NEVER; + if (PairSize(cookie) > box.maxSize || box.maxSize == 0) return ERROR_OUT_OF_SIZE; + std::optional maxAge; + std::optional expires; + std::string domain; + std::optional path; + std::string_view attributes = text.substr(pairEnd); + while (!attributes.empty()) { + attributes.remove_prefix(1); + const std::size_t end = std::min(attributes.find(';'), attributes.size()); + const std::string_view attribute = attributes.substr(0, end); + attributes.remove_prefix(end); + const std::size_t split = attribute.find('='); + const std::string_view key = Trim(attribute.substr(0, split)); + const std::string_view value = split == std::string_view::npos ? std::string_view{} : Trim(attribute.substr(split + 1)); + if (EqualsIgnoreCase(key, "expires")) { + if (const auto parsed = ParseCookieDate(value)) expires = parsed; + } else if (EqualsIgnoreCase(key, "max-age")) { + if (const auto parsed = ParseMaxAge(value, now)) maxAge = parsed; + } else if (EqualsIgnoreCase(key, "domain")) { + if (!value.empty()) domain = LowerCopy(value.front() == '.' ? value.substr(1) : value); + } else if (EqualsIgnoreCase(key, "path")) { + path = !value.empty() && value.front() == '/' ? std::string(value) : DefaultPath(url.path); + } else if (EqualsIgnoreCase(key, "secure")) { + cookie.secure = true; + } + } + if (maxAge) cookie.expiry = *maxAge; + else if (expires) cookie.expiry = *expires; + if (!domain.empty() && !DomainMatches(url.host, domain)) return ERROR_REJECTED; + cookie.domain = domain.empty() ? url.host : domain; + cookie.path = path ? *path : DefaultPath(url.path); + const auto existing = std::find_if(box.cookies.begin(), box.cookies.end(), [&](const Cookie& stored) { + return stored.name == cookie.name && stored.domain == cookie.domain && stored.path == cookie.path; + }); + if (existing != box.cookies.end()) box.cookies.erase(existing); + if (cookie.expiry <= now) return 0; + RemoveExpired(box, now); + MakeRoom(box, cookie.domain); + box.cookies.push_back(std::move(cookie)); + RecordUsage(box); + return 0; +} + +void AppendCookie(std::string& header, const Cookie& cookie) { + if (!header.empty()) header += "; "; + if (cookie.name.empty()) { + header += cookie.value; + return; + } + header += cookie.name; + if (!cookie.hasValue) return; + header += '='; + header += cookie.value; +} + +CookieBox* FindBox(int cookieBoxId) { + if (!Http2CookieBoxExists(cookieBoxId)) return nullptr; + return &g_cookieBoxes[cookieBoxId]; +} + +} + +void Http2ForgetCookies(int cookieBoxId) { + std::lock_guard lock(g_cookiesMutex); + g_cookieBoxes.erase(cookieBoxId); +} + +void Http2FlushCookies(int cookieBoxId) { + std::lock_guard lock(g_cookiesMutex); + const auto box = g_cookieBoxes.find(cookieBoxId); + if (box != g_cookieBoxes.end()) box->second.cookies.clear(); +} + +extern "C" { + +int APS5_VABI sceHttp2AddCookie(int cookieBoxId, const char* url, const char* cookie, std::size_t cookieLength) { + std::lock_guard lock(g_cookiesMutex); + CookieBox* box = FindBox(cookieBoxId); + if (!box) return ERROR_INVALID_ID; + if (!url || !cookie || cookieLength == 0) return ERROR_INVALID_VALUE; + Url parsed{}; + if (const int result = ParseUrl(url, parsed); result != 0) return result; + return StoreCookie(*box, parsed, std::string_view(cookie, cookieLength), Now()); +} + +int APS5_VABI sceHttp2GetCookie(int cookieBoxId, const char* url, char* cookie, std::size_t* required, std::size_t prepared) { + std::lock_guard lock(g_cookiesMutex); + CookieBox* box = FindBox(cookieBoxId); + if (!box) return ERROR_INVALID_ID; + if (!url) return ERROR_INVALID_VALUE; + Url parsed{}; + if (const int result = ParseUrl(url, parsed); result == ERROR_REJECTED) return ERROR_NOT_FOUND; + else if (result != 0) return result; + RemoveExpired(*box, Now()); + std::vector matches; + for (const Cookie& stored : box->cookies) { + if (DomainMatches(parsed.host, stored.domain) && PathMatches(parsed.path, stored.path) && (!stored.secure || parsed.secure)) matches.push_back(&stored); + } + if (matches.empty()) return ERROR_NOT_FOUND; + if (!cookie && !required) return ERROR_INVALID_VALUE; + std::stable_sort(matches.begin(), matches.end(), [](const Cookie* left, const Cookie* right) { return left->path.size() > right->path.size(); }); + for (auto run = matches.begin(); run != matches.end();) { + const auto end = std::find_if(run, matches.end(), [&](const Cookie* match) { return match->path.size() != (*run)->path.size(); }); + std::rotate(run, run + 1, end); + run = end; + } + std::string header; + for (const Cookie* match : matches) AppendCookie(header, *match); + const std::size_t size = header.size() + HEADER_SLACK; + if (!cookie) { + *required = size; + return 0; + } + if (prepared < size) { + if (prepared != 0) cookie[0] = '\0'; + return ERROR_OUT_OF_SIZE; + } + header.append({'\0', ' ', '\0'}); + std::memcpy(cookie, header.data(), size); + if (required) *required = size; + return 0; +} + +int APS5_VABI sceHttp2GetCookieStats(int cookieBoxId, Http2CookieStats* stats) { + std::lock_guard lock(g_cookiesMutex); + if (!stats) return ERROR_INVALID_VALUE; + CookieBox* box = FindBox(cookieBoxId); + if (!box) return ERROR_INVALID_ID; + RemoveExpired(*box, Now()); + stats->currentInuseSize = InuseSize(*box); + stats->currentInuseNum = static_cast(box->cookies.size()); + stats->maxInuseSize = box->maxInuseSize; + stats->maxInuseNum = box->maxInuseNum; + stats->removedNum = box->removedNum; + return 0; +} + +int APS5_VABI sceHttp2SetCookieMaxNum(int cookieBoxId, std::uint32_t num) { + std::lock_guard lock(g_cookiesMutex); + CookieBox* box = FindBox(cookieBoxId); + if (!box) return ERROR_INVALID_ID; + box->maxNum = num; + return 0; +} + +int APS5_VABI sceHttp2SetCookieMaxNumPerDomain(int cookieBoxId, std::uint32_t num) { + std::lock_guard lock(g_cookiesMutex); + CookieBox* box = FindBox(cookieBoxId); + if (!box) return ERROR_INVALID_ID; + box->maxNumPerDomain = num; + return 0; +} + +int APS5_VABI sceHttp2SetCookieMaxSize(int cookieBoxId, std::uint32_t size) { + std::lock_guard lock(g_cookiesMutex); + CookieBox* box = FindBox(cookieBoxId); + if (!box) return ERROR_INVALID_ID; + box->maxSize = size; + return 0; +} + +} diff --git a/core/libs/prx/libSceHttp2/Cookies.hpp b/core/libs/prx/libSceHttp2/Cookies.hpp new file mode 100644 index 0000000000..44a8587c3c --- /dev/null +++ b/core/libs/prx/libSceHttp2/Cookies.hpp @@ -0,0 +1,5 @@ +#pragma once + +bool Http2CookieBoxExists(int cookieBoxId); +void Http2ForgetCookies(int cookieBoxId); +void Http2FlushCookies(int cookieBoxId); diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index 7cb14fc27a..c5ff9a1b43 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -3,20 +3,17 @@ #include #include #include "SceTypes.hpp" +#include "Transport.hpp" +#include "Cookies.hpp" +#include "prx/libkernel/include/Api.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); @@ -26,7 +23,13 @@ struct Http2Handle { Http2Kind kind; int owner; std::size_t poolSize; - std::shared_ptr cookieMemory; + 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,341 +46,313 @@ 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) { +bool Http2CookieBoxExists(int cookieBoxId) { std::lock_guard lock(g_handlesMutex); - return FindAnyHandle(id) != nullptr; + return FindHandle(cookieBoxId, Http2Kind::CookieBox) != 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); - } +template +static int Update(int id, const char* function, TUpdate update) { + std::lock_guard lock(g_handlesMutex); + 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; + Http2FlushCookies(id); return 0; } int APS5_VABI sceHttp2CreateCookieBox(std::size_t poolSize) { if (poolSize < 0x10000) return static_cast(0x817b121du); - if (poolSize > std::numeric_limits::max() - 0x3fff) throw std::overflow_error("sceHttp2CreateCookieBox: pool size overflow"); - const auto allocationSize = (poolSize + 0x3fff) & ~std::size_t{0x3fff}; - auto memory = std::make_shared(allocationSize); + const auto mappedSize = (poolSize + 0x3fff) & ~std::size_t{0x3fff}; + void* address = nullptr; + if (const int result = sceKernelMapNamedFlexibleMemory(&address, mappedSize, 3, 0, "SceHttp2CookieBox"); result != 0) return result; + std::shared_ptr memory(address, [mappedSize](void* mapped) { sceKernelMunmap(reinterpret_cast(mapped), mappedSize); }); 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; - g_handles.erase(cookie_box_id); + { + std::lock_guard lock(g_handlesMutex); + if (!FindHandle(cookie_box_id, Http2Kind::CookieBox)) return ERROR_INVALID_ID; + g_handles.erase(cookie_box_id); + } + Http2ForgetCookies(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) { @@ -409,11 +384,6 @@ int APS5_VABI sceHttp2WebSocketSetPingTimeout(std::int32_t requestId, std::uint3 NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttp2AddCookie(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHttp2AuthCacheFlush(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -439,21 +409,11 @@ int APS5_VABI sceHttp2GetAutoRedirect(void) { return 0; } -int APS5_VABI sceHttp2GetCookie(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHttp2GetCookieBox(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttp2GetCookieStats(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHttp2RedirectCacheFlush(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -469,21 +429,6 @@ int APS5_VABI sceHttp2SetAuthInfoCallback(void) { return 0; } -int APS5_VABI sceHttp2SetCookieMaxNum(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHttp2SetCookieMaxNumPerDomain(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHttp2SetCookieMaxSize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHttp2SetCookieRecvCallback(void) { NotImplemented_nid_no_patch(__func__); return 0; 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/libSceHttp2/include/Http2.hpp b/core/libs/prx/libSceHttp2/include/Http2.hpp index bf7b34e6a7..678aec1f08 100644 --- a/core/libs/prx/libSceHttp2/include/Http2.hpp +++ b/core/libs/prx/libSceHttp2/include/Http2.hpp @@ -9,6 +9,14 @@ struct Http2MemoryPoolStats { int32_t reserved; }; +struct Http2CookieStats { + std::size_t currentInuseSize; + std::uint32_t currentInuseNum; + std::size_t maxInuseSize; + std::uint32_t maxInuseNum; + std::uint32_t removedNum; +}; + using Http2WebSocketTextCallback = void (APS5_VABI*)(std::int32_t requestId, const char* text, std::uint32_t flags, void* userData); using Http2WebSocketDataCallback = void (APS5_VABI*)(std::int32_t requestId, const void* data, std::size_t dataSize, std::uint32_t flags, void* userData); @@ -63,21 +71,21 @@ int APS5_VABI sceHttp2WebSocketSendTextMessageAsync(std::int32_t requestId, cons int APS5_VABI sceHttp2WebSocketSendDataMessageAsync(std::int32_t requestId, const void* data, std::size_t dataSize, std::uint8_t flags, const Http2AsyncOption* asyncOption, const void* option); int APS5_VABI sceHttp2WebSocketSetPingInterval(std::int32_t requestId, std::uint32_t intervalMicroseconds); int APS5_VABI sceHttp2WebSocketSetPingTimeout(std::int32_t requestId, std::uint32_t timeoutMicroseconds); -int APS5_VABI sceHttp2AddCookie(void); +int APS5_VABI sceHttp2AddCookie(int cookieBoxId, const char* url, const char* cookie, std::size_t cookieLength); int APS5_VABI sceHttp2AuthCacheFlush(void); int APS5_VABI sceHttp2CookieExport(void); int APS5_VABI sceHttp2CookieImport(void); int APS5_VABI sceHttp2GetAuthEnabled(void); int APS5_VABI sceHttp2GetAutoRedirect(void); -int APS5_VABI sceHttp2GetCookie(void); +int APS5_VABI sceHttp2GetCookie(int cookieBoxId, const char* url, char* cookie, std::size_t* required, std::size_t prepared); int APS5_VABI sceHttp2GetCookieBox(void); -int APS5_VABI sceHttp2GetCookieStats(void); +int APS5_VABI sceHttp2GetCookieStats(int cookieBoxId, Http2CookieStats* stats); int APS5_VABI sceHttp2RedirectCacheFlush(void); int APS5_VABI sceHttp2RemoveRequestHeader(void); int APS5_VABI sceHttp2SetAuthInfoCallback(void); -int APS5_VABI sceHttp2SetCookieMaxNum(void); -int APS5_VABI sceHttp2SetCookieMaxNumPerDomain(void); -int APS5_VABI sceHttp2SetCookieMaxSize(void); +int APS5_VABI sceHttp2SetCookieMaxNum(int cookieBoxId, std::uint32_t num); +int APS5_VABI sceHttp2SetCookieMaxNumPerDomain(int cookieBoxId, std::uint32_t num); +int APS5_VABI sceHttp2SetCookieMaxSize(int cookieBoxId, std::uint32_t size); int APS5_VABI sceHttp2SetCookieRecvCallback(void); int APS5_VABI sceHttp2SetCookieSendCallback(void); int APS5_VABI sceHttp2SetPreSendCallback(void); 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 b7f0f3ef35..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; } @@ -139,7 +983,8 @@ int APS5_VABI sceHttpCacheDebugSetUrl() { 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/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 96be9446d3..f17760d179 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -78,6 +78,7 @@ if(BUILD_TESTING) 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/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 7a8ad2999c..3621baad39 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Runtime.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Aes.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Error.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ocsp.cpp) +add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Runtime.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Aes.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Error.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ocsp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Stack.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ui.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rsa.cpp) target_include_directories(libSceLibreSsl PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibreSsl PRIVATE mbedcrypto) configure_windows_unwind(libSceLibreSsl) @@ -88,6 +88,39 @@ if(BUILD_TESTING) 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() + + add_test_executable(guest_libressl_rsa_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslRsa.cpp) + target_include_directories(guest_libressl_rsa_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_rsa_tests PRIVATE libSceLibreSsl) + configure_windows_unwind(guest_libressl_rsa_tests) + add_test(NAME guest_libressl_rsa COMMAND guest_libressl_rsa_tests) + set_tests_properties(guest_libressl_rsa PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_rsa PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Cipher.cpp) @@ -104,3 +137,44 @@ if(BUILD_TESTING) "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 93387ef0ba..00ace935b0 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,6 +14,13 @@ 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}; } @@ -204,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) { @@ -220,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) { @@ -903,11 +909,6 @@ int APS5_VABI PKCS12_parse_nid_postfix(void) { return 0; } -int APS5_VABI RSA_flags_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI RSA_free_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -953,11 +954,6 @@ int APS5_VABI RSA_sign_nid_postfix(void) { return 0; } -int APS5_VABI RSA_size_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI RSA_verify_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1158,11 +1154,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; @@ -1383,9 +1374,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) { @@ -1408,16 +1398,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; @@ -1433,46 +1413,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; @@ -1798,24 +1738,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 c7edd129f6..a114265d0c 100644 --- a/core/libs/prx/libSceLibreSsl/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl/include/Api.hpp @@ -16,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; @@ -51,6 +56,64 @@ 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; +}; + +struct LibreSslRsaMethod { + const char* name; + void* operations[8]; + int flags; + char* appData; + void* sign; + void* verify; + void* keygen; +}; + +struct LibreSslRsa { + int pad; + std::int64_t version; + const LibreSslRsaMethod* meth; + void* engine; + void* n; + void* e; + void* d; + void* p; + void* q; + void* dmp1; + void* dmq1; + void* iqmp; + void* exData; + int references; + int flags; + void* methodModN; + void* methodModP; + void* methodModQ; + void* blinding; + void* mtBlinding; +}; + extern "C" { std::int64_t APS5_VABI ASN1_INTEGER_get_nid_postfix(const Asn1String* value); @@ -95,12 +158,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); @@ -240,7 +303,7 @@ int APS5_VABI PEM_write_bio_X509_nid_postfix(void); int APS5_VABI PKCS12_PBE_add_nid_postfix(void); int APS5_VABI PKCS12_free_nid_postfix(void); int APS5_VABI PKCS12_parse_nid_postfix(void); -int APS5_VABI RSA_flags_nid_postfix(void); +int APS5_VABI RSA_flags_nid_postfix(const LibreSslRsa* rsa); int APS5_VABI RSA_free_nid_postfix(void); int APS5_VABI RSA_generate_key_ex_nid_postfix(void); int APS5_VABI RSA_new_nid_postfix(void); @@ -250,7 +313,7 @@ int APS5_VABI RSA_private_encrypt_nid_postfix(void); int APS5_VABI RSA_public_decrypt_nid_postfix(void); int APS5_VABI RSA_public_encrypt_nid_postfix(void); int APS5_VABI RSA_sign_nid_postfix(void); -int APS5_VABI RSA_size_nid_postfix(void); +int APS5_VABI RSA_size_nid_postfix(const LibreSslRsa* rsa); 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(const void* cipher, std::int32_t* algorithmBits); @@ -294,7 +357,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); @@ -339,24 +402,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); @@ -410,10 +473,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/src/Asn1.cpp b/core/libs/prx/libSceLibreSsl/src/Asn1.cpp index 304ad8ad93..409df8adac 100644 --- a/core/libs/prx/libSceLibreSsl/src/Asn1.cpp +++ b/core/libs/prx/libSceLibreSsl/src/Asn1.cpp @@ -87,17 +87,17 @@ std::int64_t APS5_VABI ASN1_INTEGER_get_nid_postfix(const Asn1String* value) { } unsigned char* APS5_VABI ASN1_STRING_data_nid_postfix(Asn1String* value) { - if (!value) NotImplemented_nid_no_patch(__func__); + if (!value) APS5_INVALID_ARG_EX; return value->data; } int APS5_VABI ASN1_STRING_length_nid_postfix(const Asn1String* value) { - if (!value) NotImplemented_nid_no_patch(__func__); + if (!value) APS5_INVALID_ARG_EX; return value->length; } int APS5_VABI ASN1_STRING_type_nid_postfix(Asn1String* value) { - if (!value) NotImplemented_nid_no_patch(__func__); + if (!value) APS5_INVALID_ARG_EX; return value->type; } diff --git a/core/libs/prx/libSceLibreSsl/src/Rsa.cpp b/core/libs/prx/libSceLibreSsl/src/Rsa.cpp new file mode 100644 index 0000000000..66ff820ecc --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Rsa.cpp @@ -0,0 +1,54 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include "prx/libc/include/General.hpp" + +struct LibreSslBignum { + std::uint64_t* d; + int top; + int dmax; + int neg; + int flags; +}; + +static_assert(sizeof(LibreSslBignum) == 24); +static_assert(offsetof(LibreSslBignum, top) == 8); +static_assert(sizeof(LibreSslRsaMethod) == 112); +static_assert(offsetof(LibreSslRsaMethod, flags) == 72); +static_assert(sizeof(LibreSslRsa) == 152); +static_assert(offsetof(LibreSslRsa, meth) == 16); +static_assert(offsetof(LibreSslRsa, n) == 32); +static_assert(offsetof(LibreSslRsa, flags) == 108); + +namespace { + +int ByteCount(const LibreSslBignum* number, const char* function) { + if (!number) throw std::invalid_argument(std::string(function) + ": null modulus"); + if (number->top == 0) return 0; + if (number->top < 0) throw std::invalid_argument(std::string(function) + ": negative modulus word count"); + if (!number->d) throw std::invalid_argument(std::string(function) + ": null modulus words"); + const std::int64_t bits = static_cast(number->top - 1) * 64 + std::bit_width(number->d[number->top - 1]); + if (bits > INT_MAX - 7) throw std::invalid_argument(std::string(function) + ": modulus bit count overflows int"); + return static_cast((bits + 7) / 8); +} + +} + +extern "C" { + +int APS5_VABI RSA_flags_nid_postfix(const LibreSslRsa* rsa) { + if (!rsa) return 0; + if (!rsa->meth) throw std::invalid_argument(std::string(__func__) + ": null method"); + return rsa->meth->flags; +} + +int APS5_VABI RSA_size_nid_postfix(const LibreSslRsa* rsa) { + if (!rsa) throw std::invalid_argument(std::string(__func__) + ": null key"); + return ByteCount(static_cast(rsa->n), __func__); +} + +} 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 index 354f36a0c9..e99142e9ad 100644 --- a/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp +++ b/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp @@ -36,6 +36,16 @@ void RequireThrows(TAction action, const char* message) { 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); @@ -65,9 +75,9 @@ void Accessors() { 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"); - RequireThrows([] { ASN1_STRING_data_nid_postfix(nullptr); }, "null data accessor input"); - RequireThrows([] { ASN1_STRING_length_nid_postfix(nullptr); }, "null length accessor input"); - RequireThrows([] { ASN1_STRING_type_nid_postfix(nullptr); }, "null type accessor input"); + 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() { diff --git a/core/libs/prx/libSceLibreSsl3/CMakeLists.txt b/core/libs/prx/libSceLibreSsl3/CMakeLists.txt index 1a3d213378..9f5ecac040 100644 --- a/core/libs/prx/libSceLibreSsl3/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl3/CMakeLists.txt @@ -1,6 +1,6 @@ -add_library(libSceLibreSsl3 SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp) +add_library(libSceLibreSsl3 SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/IntegerDecoder.cpp) target_include_directories(libSceLibreSsl3 PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libSceLibreSsl3 PRIVATE libSceLibreSsl) +target_link_libraries(libSceLibreSsl3 PRIVATE libSceLibreSsl libc) configure_windows_unwind(libSceLibreSsl3) if(BUILD_TESTING) @@ -15,8 +15,14 @@ if(BUILD_TESTING) configure_windows_unwind(guest_libressl3_digest_tests) add_test(NAME guest_libressl3_digest COMMAND guest_libressl3_digest_tests) set_tests_properties(guest_libressl3_digest PROPERTIES TIMEOUT 30) + add_test_executable(guest_libressl3_asn1_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Asn1.cpp) + target_include_directories(guest_libressl3_asn1_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl3_asn1_tests PRIVATE libSceLibreSsl3 libc) + configure_windows_unwind(guest_libressl3_asn1_tests) + add_test(NAME guest_libressl3_asn1 COMMAND guest_libressl3_asn1_tests) + set_tests_properties(guest_libressl3_asn1 PROPERTIES TIMEOUT 30) if(WIN32) - set_tests_properties(guest_crypto_memcmp guest_libressl3_digest PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_crypto_memcmp guest_libressl3_digest guest_libressl3_asn1 PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceLibreSsl3/Export.cpp b/core/libs/prx/libSceLibreSsl3/Export.cpp index dfd0b1b483..e2fa6b0ef4 100644 --- a/core/libs/prx/libSceLibreSsl3/Export.cpp +++ b/core/libs/prx/libSceLibreSsl3/Export.cpp @@ -396,11 +396,6 @@ int APS5_VABI X509_up_ref_nid_postfix(void) { return 0; } -int APS5_VABI d2i_ASN1_INTEGER_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI d2i_ECParameters_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl3/include/Api.hpp b/core/libs/prx/libSceLibreSsl3/include/Api.hpp index ff2a535fd7..396e514cae 100644 --- a/core/libs/prx/libSceLibreSsl3/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl3/include/Api.hpp @@ -1,6 +1,7 @@ #pragma once #include "SceTypes.hpp" +#include "prx/libSceLibreSsl/include/Api.hpp" extern "C" { @@ -81,7 +82,7 @@ int APS5_VABI X509_get_version_nid_postfix(void); int APS5_VABI X509_getm_notAfter_nid_postfix(void); int APS5_VABI X509_getm_notBefore_nid_postfix(void); int APS5_VABI X509_up_ref_nid_postfix(void); -int APS5_VABI d2i_ASN1_INTEGER_nid_postfix(void); +void* APS5_VABI d2i_ASN1_INTEGER_nid_postfix(void** output, const unsigned char** input, std::int64_t length); int APS5_VABI d2i_ECParameters_nid_postfix(void); int APS5_VABI d2i_PKCS7_nid_postfix(void); int APS5_VABI d2i_PKCS8_PRIV_KEY_INFO_bio_nid_postfix(void); diff --git a/core/libs/prx/libSceLibreSsl3/src/IntegerDecoder.cpp b/core/libs/prx/libSceLibreSsl3/src/IntegerDecoder.cpp new file mode 100644 index 0000000000..43d0661c11 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl3/src/IntegerDecoder.cpp @@ -0,0 +1,126 @@ +#include "prx/libSceLibreSsl3/libSceLibreSsl3.hpp" +#include "prx/libc/libc.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include + + +namespace { + + +static_assert(sizeof(Asn1String) == 24); +static_assert(offsetof(Asn1String, data) == 8); +static_assert(offsetof(Asn1String, flags) == 16); + +unsigned char ReadByte(std::span& input) { + if (input.empty()) NotImplemented_nid_no_patch("d2i_ASN1_INTEGER: truncated header"); + const auto byte = input.front(); + input = input.subspan(1); + return byte; +} + +std::span ReadInteger(std::span& input) { + const auto identifier = ReadByte(input); + if ((identifier & 0xe0) != 0) NotImplemented_nid_no_patch("d2i_ASN1_INTEGER: nonprimitive integer"); + std::uint32_t tag = identifier & 0x1f; + if (tag == 0x1f) { + tag = 0; + unsigned char byte; + do { + byte = ReadByte(input); + if (tag > (std::numeric_limits::max() >> 7)) + NotImplemented_nid_no_patch("d2i_ASN1_INTEGER: tag overflow"); + tag = (tag << 7) | (byte & 0x7f); + } while (byte & 0x80); + } + if (tag != 2) NotImplemented_nid_no_patch("d2i_ASN1_INTEGER: wrong tag"); + const auto lengthByte = ReadByte(input); + std::size_t length = lengthByte; + if (lengthByte >= 0x80) { + const auto bytes = lengthByte & 0x7f; + if (bytes == 0 || bytes == 0x7f) NotImplemented_nid_no_patch("d2i_ASN1_INTEGER: invalid length"); + length = 0; + for (int index = 0; index < bytes; ++index) { + if (length > (std::numeric_limits::max() >> 8)) + NotImplemented_nid_no_patch("d2i_ASN1_INTEGER: length overflow"); + length = (length << 8) | ReadByte(input); + } + } + if (length == 0 || length > input.size() || length > static_cast(std::numeric_limits::max())) + NotImplemented_nid_no_patch("d2i_ASN1_INTEGER: invalid content length"); + const auto content = input.first(length); + input = input.subspan(length); + return content; +} + +void FreeInteger(void* value) { + if (!value) return; + auto& integer = *static_cast(value); + if (!(integer.flags & 0x10)) { + if (integer.data) { + if (integer.length < 0) NotImplemented_nid_no_patch(__func__); + auto* bytes = static_cast(integer.data); + for (int index = 0; index < integer.length; ++index) bytes[index] = 0; + free_nid_postfix(integer.data); + } + } + integer.flags &= ~std::int64_t{0x10}; + integer.data = nullptr; + integer.length = 0; + free_nid_postfix(value); +} + + +} // namespace + +extern "C" { + +void* APS5_VABI d2i_ASN1_INTEGER_nid_postfix(void** output, const unsigned char** input, std::int64_t length) { + if (length < 0) return nullptr; + if (!input || !*input) NotImplemented_nid_no_patch(__func__); + std::span remaining(*input, static_cast(length)); + auto content = ReadInteger(remaining); + const bool negative = (content.front() & 0x80) != 0; + const unsigned char padding = negative ? 0xff : 0; + if (content.size() > 1 && content.front() == padding) { + bool keepPadding = negative; + for (std::size_t index = 1; index < content.size(); ++index) + keepPadding = keepPadding && content[index] == 0; + if (!keepPadding) { + content = content.subspan(1); + if (((content.front() & 0x80) != 0) == negative) + NotImplemented_nid_no_patch("d2i_ASN1_INTEGER: redundant sign padding"); + } + } + if (output) { + FreeInteger(*output); + *output = nullptr; + } + auto* data = static_cast(malloc_nid_postfix(content.size())); + if (!data) NotImplemented_nid_no_patch("d2i_ASN1_INTEGER: allocation failure"); + std::memcpy(data, content.data(), content.size()); + if (negative) { + unsigned int carry = 1; + for (std::size_t index = content.size(); index != 0; --index) { + const unsigned int digit = (data[index - 1] ^ 0xff) + carry; + data[index - 1] = static_cast(digit); + carry = digit >> 8; + } + } + auto* integer = static_cast(malloc_nid_postfix(sizeof(Asn1String))); + if (!integer) { + auto* bytes = static_cast(data); + for (std::size_t index = 0; index < content.size(); ++index) bytes[index] = 0; + free_nid_postfix(data); + NotImplemented_nid_no_patch("d2i_ASN1_INTEGER: allocation failure"); + } + *integer = {static_cast(content.size()), negative ? 0x102 : 2, data, 0}; + if (output) *output = integer; + *input += static_cast(length) - remaining.size(); + return integer; +} + +} diff --git a/core/libs/prx/libSceLibreSsl3/tests/Asn1.cpp b/core/libs/prx/libSceLibreSsl3/tests/Asn1.cpp new file mode 100644 index 0000000000..9d6c37414d --- /dev/null +++ b/core/libs/prx/libSceLibreSsl3/tests/Asn1.cpp @@ -0,0 +1,209 @@ +#include "prx/libSceLibreSsl3/libSceLibreSsl3.hpp" +#include "prx/libc/libc.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace { + +using String = Asn1String; + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "LibreSSL3 ASN.1 check failed at line %u\n", location.line()); + std::abort(); +} + +std::unordered_map allocations; +std::size_t freeCalls = 0; +void* dataToClear = nullptr; +std::size_t clearLength = 0; + +void* APS5_VABI Allocate_nid_no_patch(std::size_t size) { + void* result = std::malloc(size); + Require(result != nullptr); + allocations.emplace(result, size); + return result; +} + +void APS5_VABI Free_nid_no_patch(void* pointer) { + if (!pointer) return; + const auto found = allocations.find(pointer); + Require(found != allocations.end()); + if (found->second == sizeof(String)) { + const auto* value = static_cast(pointer); + Require(value->length == 0 && value->data == nullptr && !(value->flags & 0x10)); + } + Require(allocations.erase(pointer) == 1); + if (pointer == dataToClear) { + const auto* bytes = static_cast(pointer); + Require(std::all_of(bytes, bytes + clearLength, [](auto byte) { return byte == 0; })); + dataToClear = nullptr; + } + ++freeCalls; + std::free(pointer); +} + +void* APS5_VABI UnexpectedCalloc_nid_no_patch(std::size_t, std::size_t) { throw std::runtime_error("unexpected calloc"); } +void* APS5_VABI UnexpectedReallocate_nid_no_patch(void*, std::size_t) { throw std::runtime_error("unexpected realloc"); } +void* APS5_VABI UnexpectedAlign_nid_no_patch(std::size_t, std::size_t) { throw std::runtime_error("unexpected aligned allocation"); } +void* APS5_VABI UnexpectedRealign_nid_no_patch(void*, std::size_t, std::size_t) { throw std::runtime_error("unexpected aligned reallocation"); } +int APS5_VABI UnexpectedPosixAlign_nid_no_patch(void**, std::size_t, std::size_t) { throw std::runtime_error("unexpected posix_memalign"); } + +void Release(String* value) { + if (!value) return; + const auto data = value->data; + value->length = 0; + value->data = nullptr; + value->flags &= ~std::int64_t{0x10}; + free_nid_postfix(data); + free_nid_postfix(value); +} + +void Check(std::span encoded, std::span magnitude, bool negative = false) { + std::vector bytes(encoded.begin(), encoded.end()); + bytes.push_back(0xa5); + const unsigned char* cursor = bytes.data(); + void* output = nullptr; + auto* value = static_cast(d2i_ASN1_INTEGER_nid_postfix(&output, &cursor, bytes.size())); + Require(value && output == value && cursor == bytes.data() + encoded.size()); + Require(*cursor == 0xa5 && value->length == static_cast(magnitude.size())); + Require(value->type == (negative ? 0x102 : 2) && value->flags == 0); + Require(value->data != bytes.data()); + Require(std::equal(magnitude.begin(), magnitude.end(), value->data)); + Require(std::equal(encoded.begin(), encoded.end(), bytes.begin())); + Release(value); +} + +void Check(std::initializer_list encoded, std::initializer_list magnitude, bool negative = false) { + Check(std::span(encoded.begin(), encoded.size()), std::span(magnitude.begin(), magnitude.size()), negative); +} + +void Integers() { + Check({2, 1, 0}, {0}); + Check({2, 2, 0, 0x80}, {0x80}); + Check({2, 1, 0xff}, {1}, true); + Check({2, 1, 0x80}, {0x80}, true); + Check({2, 2, 0xff, 0x7f}, {0x81}, true); + Check({2, 2, 0xff, 0}, {1, 0}, true); + Check({2, 3, 0xff, 0, 0}, {1, 0, 0}, true); + Check({2, 0x82, 0, 1, 0x7f}, {0x7f}); + Check({0x1f, 0x80, 2, 1, 0x7f}, {0x7f}); + Check({2, 9, 0, 0x80, 0, 0, 0, 0, 0, 0, 0}, {0x80, 0, 0, 0, 0, 0, 0, 0}); + Check({2, 8, 0x80, 0, 0, 0, 0, 0, 0, 0}, {0x80, 0, 0, 0, 0, 0, 0, 0}, true); + std::vector large(131, 0xff); + large[0] = 2; + large[1] = 0x81; + large[2] = 128; + large[3] = 0x7f; + std::vector expected(128, 0xff); + expected[0] = 0x7f; + Check(large, expected); + for (int number = -32768; number <= 32767; ++number) { + const auto bits = static_cast(number); + std::array encoded{2, 2, static_cast(bits >> 8), static_cast(bits)}; + std::span content(encoded); + if (number >= -128 && number <= 127) { + encoded[1] = 1; + encoded[2] = encoded[3]; + content = content.first(3); + } + const auto absolute = static_cast(number < 0 ? -number : number); + std::array magnitude{static_cast(absolute >> 8), static_cast(absolute)}; + Check(content, std::span(magnitude).subspan(absolute < 256 ? 1 : 0), number < 0); + } +} + +void Ownership() { + const unsigned char encoded[] = {2, 1, 42}; + const unsigned char* cursor = encoded; + auto* value = static_cast(d2i_ASN1_INTEGER_nid_postfix(nullptr, &cursor, sizeof(encoded))); + void* output = value; + dataToClear = value->data; + clearLength = 1; + const auto previousFreeCalls = freeCalls; + cursor = encoded; + value = static_cast(d2i_ASN1_INTEGER_nid_postfix(&output, &cursor, sizeof(encoded))); + Require(output == value && value->data[0] == 42 && freeCalls == previousFreeCalls + 2 && dataToClear == nullptr); + Release(value); + auto* borrowed = static_cast(Allocate_nid_no_patch(sizeof(String))); + unsigned char bytes[] = {0xa5, 0x5a}; + *borrowed = {2, 2, bytes, 0x10}; + output = borrowed; + cursor = encoded; + value = static_cast(d2i_ASN1_INTEGER_nid_postfix(&output, &cursor, sizeof(encoded))); + Require(bytes[0] == 0xa5 && bytes[1] == 0x5a); + Release(value); + for (const auto length : {0, -1, std::numeric_limits::min()}) { + auto* empty = static_cast(Allocate_nid_no_patch(sizeof(String))); + *empty = {length, 2, nullptr, 0}; + output = empty; + cursor = encoded; + const auto before = freeCalls; + value = static_cast(d2i_ASN1_INTEGER_nid_postfix(&output, &cursor, sizeof(encoded))); + Require(value == output && value->data[0] == 42 && freeCalls == before + 1); + Release(value); + } + Require(allocations.empty()); + +} + +void BerExtremes() { + std::vector negative(131, 0); + negative[0] = 2; + negative[1] = 0x81; + negative[2] = 128; + negative[3] = 0xff; + std::vector magnitude(128, 0); + magnitude[0] = 1; + Check(negative, magnitude, true); + std::vector positive(132, 0); + positive[0] = 2; + positive[1] = 0x81; + positive[2] = 129; + positive[4] = 0x80; + magnitude[0] = 0x80; + Check(positive, magnitude); + std::vector tag(1004, 0x80); + tag[0] = 0x1f; + tag[1001] = 2; + tag[1002] = 1; + tag[1003] = 42; + const unsigned char expected[] = {42}; + Check(tag, expected); + std::vector length(129, 0); + length[0] = 2; + length[1] = 0xfe; + length[127] = 1; + length[128] = 42; + Check(length, expected); +} + +} // namespace + +int main() { + std::array api{ + reinterpret_cast(Allocate_nid_no_patch), reinterpret_cast(Free_nid_no_patch), reinterpret_cast(UnexpectedCalloc_nid_no_patch), + reinterpret_cast(UnexpectedReallocate_nid_no_patch), reinterpret_cast(UnexpectedAlign_nid_no_patch), reinterpret_cast(UnexpectedRealign_nid_no_patch), + reinterpret_cast(UnexpectedPosixAlign_nid_no_patch)}; + ApplicationHeapRegister_nid_no_patch(api.data()); + Integers(); + BerExtremes(); + Ownership(); + Require(allocations.empty()); + std::puts("libSceLibreSsl3 ASN.1 tests passed"); +} 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 f900648cf2..acc487792e 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -4,6 +4,39 @@ target_link_libraries(libSceNet PRIVATE libSceNetCtl) configure_windows_unwind(libSceNet) if(BUILD_TESTING) + add_test_executable(net_ipv6only_patched_tests tests/Ipv6OnlyPatched.cpp + ${LIBS_INCLUDE_DIR}/nid/src/NidCompute.cpp + ${LIBS_INCLUDE_DIR}/nid/src/Sha1.cpp) + target_include_directories(net_ipv6only_patched_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(net_ipv6only_patched_tests PRIVATE ${CMAKE_DL_LIBS}) + add_dependencies(net_ipv6only_patched_tests libs) + if(MINGW) + target_compile_options(net_ipv6only_patched_tests PRIVATE -fasynchronous-unwind-tables) + target_link_options(net_ipv6only_patched_tests PRIVATE -static -static-libgcc -static-libstdc++) + endif() + add_test(NAME net_ipv6only_patched COMMAND net_ipv6only_patched_tests ${PATCHED_DIR}) + set_tests_properties(net_ipv6only_patched PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77) + add_test_executable(net_ipv6only_tests tests/Ipv6Only.cpp) + target_include_directories(net_ipv6only_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(net_ipv6only_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(net_ipv6only_tests) + add_test(NAME net_ipv6only COMMAND net_ipv6only_tests) + set_tests_properties(net_ipv6only PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77) + if(WIN32) + target_link_libraries(net_ipv6only_tests PRIVATE ws2_32) + set_tests_properties(net_ipv6only PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + 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) @@ -37,6 +70,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) @@ -95,6 +139,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}) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index ce94422d82..d2f6a103e0 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -46,6 +46,7 @@ #include "prx/libkernel/Socket/include/SocketError.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include "prx/libkernel/Socket/include/SocketOptions.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include "prx/libkernel/Pthread/include/ThreadLifecycle.hpp" #include "src/ResolverTestHooks.hpp" @@ -99,6 +100,7 @@ constexpr int NET_EINTR = 4; constexpr int NET_EBADF = 9; constexpr int NET_EACCES = 13; constexpr int NET_EFAULT = 14; +constexpr int NET_EBUSY = 16; constexpr int NET_EINVAL = 22; constexpr int NET_ENOTTY = 25; constexpr int NET_ENOSPC = 28; @@ -125,8 +127,14 @@ constexpr int NET_EHOSTUNREACH = 65; constexpr int NET_ENOTINIT = 200; constexpr int NET_ERROR_BASE = static_cast(0x80410100u); constexpr int NET_ERROR_RESOLVER_ENODNS = static_cast(0x804101E1u); +constexpr int NET_CONFIG_ERROR_NULL_FLAGS = static_cast(0x8041130Eu); constexpr int NET_RESOLVER_ABORT_FLAG_NTOA_PRESERVATION = 0x1; constexpr int NET_RESOLVER_ABORT_FLAG_ATON_PRESERVATION = 0x2; +constexpr std::uint32_t NET_IFF_UP = 0x1; +constexpr std::uint32_t NET_IFF_LOOPBACK = 0x8; +constexpr std::uint32_t NET_IFF_RUNNING = 0x40; +constexpr std::uint32_t NET_IFF_MULTICAST = 0x8000; +constexpr std::uint32_t NET_IF_LO0 = 0; constexpr int NET_AF_INET = 2; constexpr int NET_AF_INET6 = 28; @@ -138,6 +146,7 @@ constexpr int NET_EPOLL_CTL_MOD = 2; constexpr int NET_EPOLL_CTL_DEL = 3; constexpr int NET_SOL_SOCKET = 0xFFFF; constexpr int NET_IPPROTO_TCP = 6; +constexpr int NET_IPPROTO_IPV6 = 41; constexpr int NET_TCP_NODELAY = 1; constexpr int NET_TF_NODELAY = 4; constexpr int NET_SO_SNDTIMEO = 0x1005; @@ -149,13 +158,17 @@ 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; +constexpr int NET_MSG_NOSIGNAL = 0x20000; constexpr int NET_UIO_MAXIOV = 1024; #ifdef _WIN32 @@ -270,6 +283,7 @@ struct Sock { 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; @@ -349,6 +363,8 @@ int g_next_epoll = 0x4000; int g_next_pool = 1; int g_next_resolver = 0x100; bool g_net_inited = false; +bool g_net_init_param_set = false; +std::array g_net_init_param{}; std::atomic g_resolverLookupForTests{nullptr}; int* errno_slot() { @@ -545,6 +561,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); @@ -559,6 +593,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; } @@ -1227,7 +1265,7 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* } int64_t APS5_VABI sceNetSend(int s, const void* buf, size_t len, int flags) { - if (!message_flags_supported(flags, NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); + if (!message_flags_supported(flags, NET_MSG_DONTWAIT | NET_MSG_NOSIGNAL)) return fail(NET_EOPNOTSUPP); if (!buf && len) return fail(NET_EINVAL); if (len > INT_MAX) return fail(NET_EMSGSIZE); Sock socket; @@ -1237,9 +1275,15 @@ 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()); } @@ -1248,7 +1292,7 @@ int64_t APS5_VABI GuestNetSend_nid_no_patch(int s, const void* buf, size_t len, } 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 (!message_flags_supported(flags, NET_MSG_DONTWAIT | NET_MSG_NOSIGNAL)) return fail(NET_EOPNOTSUPP); if (!buf && len) return fail(NET_EINVAL); if (len > INT_MAX) return fail(NET_EMSGSIZE); Sock socket; @@ -1258,21 +1302,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__) @@ -1405,6 +1466,11 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval if (!GuestSocketIsOpen_nid_no_patch(s)) return fail(NET_EBADF); return setsockopt_nid_postfix(s, level, optname, optval, optlen) == 0 ? 0 : fail(*__error_nid_postfix()); } + if (level == NET_IPPROTO_IPV6) { + if (!optval) return fail(NET_EINVAL); + const int error = GuestSocketSetIpv6LevelOption_nid_no_patch(static_cast(socket.native->value), socket.family == NET_AF_INET6, optname, optval, optlen); + return error == 0 ? 0 : fail(error); + } if (level == NET_IPPROTO_TCP && socket.type != NET_SOCK_STREAM) return fail(NET_EINVAL); if (level == NET_IPPROTO_TCP && optname == NET_TCP_NODELAY) { if (!optval || optlen < sizeof(int)) return fail(NET_EINVAL); @@ -1425,7 +1491,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, @@ -1451,6 +1522,7 @@ 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; @@ -1471,12 +1543,45 @@ 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_IPPROTO_IPV6) { + int value = 0; + std::uint32_t length = *optlen; + const int error = GuestSocketGetIpv6LevelOption_nid_no_patch(static_cast(socket.native->value), socket.family == NET_AF_INET6, optname, &value, &length); + if (error != 0) return fail(error); + const GuestArena::HostWrite destination(length == 0 ? nullptr : optval, length); + if (!destination.Open()) return fail(NET_EFAULT); + const GuestArena::HostWrite lengthOutput(optlen, sizeof(*optlen)); + if (!lengthOutput.Open()) return fail(NET_EFAULT); + if (length != 0) std::memcpy(optval, &value, length); + *optlen = length; + 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}; @@ -1507,11 +1612,22 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint std::memcpy(optval, &value, *optlen); return 0; } - if (*optlen < sizeof(int)) return fail(NET_EINVAL); + 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; @@ -1543,8 +1659,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; } @@ -1787,8 +1903,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); @@ -2074,8 +2189,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. @@ -2088,7 +2202,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] = {}; @@ -2148,9 +2262,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; @@ -2502,7 +2614,12 @@ int APS5_VABI sceNetGetSystemMemInfo(NetSystemMemoryInfo* info, std::uint32_t fl } int APS5_VABI sceNetInitParam(const NetInitializeParameter* parameter) { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lk(g_mutex); + if (g_net_init_param_set) return fail(NET_EBUSY); + if (parameter == nullptr) return fail(NET_EINVAL); + std::memcpy(g_net_init_param.data(), parameter, g_net_init_param.size()); + g_net_init_param_set = true; + g_net_inited = true; return 0; } @@ -2705,7 +2822,10 @@ int APS5_VABI sceNetShowRoute6ForBuffer(char* buffer, std::int32_t bufferSize, s } int APS5_VABI sceNetConfigGetIfFlags(std::uint32_t interfaceId, std::uint32_t* flags) { - NotImplemented_nid_no_patch(__func__); + if (flags == nullptr) return NET_CONFIG_ERROR_NULL_FLAGS; + if (interfaceId >= 10) return -1; + if (interfaceId != NET_IF_LO0) throw std::runtime_error("sceNetConfigGetIfFlags: interface " + std::to_string(interfaceId) + " is not implemented"); + *flags = NET_IFF_UP | NET_IFF_LOOPBACK | NET_IFF_RUNNING | NET_IFF_MULTICAST; return 0; } diff --git a/core/libs/prx/libSceNet/tests/Ipv6Only.cpp b/core/libs/prx/libSceNet/tests/Ipv6Only.cpp new file mode 100644 index 0000000000..9470a5be35 --- /dev/null +++ b/core/libs/prx/libSceNet/tests/Ipv6Only.cpp @@ -0,0 +1,202 @@ +#include "prx/libkernel/libkernel.hpp" +#include "prx/libSceNet/libSceNet.hpp" +#include "SceTypes.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "IPV6_V6ONLY check failed at line %u (errno %d)\n", location.line(), *sceNetErrnoLoc()); + std::abort(); +} + +static void RequireOption(int descriptor, int expected) { + for (std::uint32_t capacity = 0; capacity <= 8; ++capacity) { + std::array output; + output.fill(0xa5); + auto length = capacity; + *sceNetErrnoLoc() = 1234; + Require(sceNetGetsockopt(descriptor, 41, 27, output.data(), &length) == 0); + Require(*sceNetErrnoLoc() == 1234 && length == std::min(capacity, sizeof(expected))); + Require(std::memcmp(output.data(), &expected, length) == 0); + for (auto index = length; index < output.size(); ++index) Require(output[index] == 0xa5); + } +} + +static void Exercise(int descriptor) { + RequireOption(descriptor, 1); + for (const int value : {0, 1, -1, std::numeric_limits::min(), 0x10}) { + *sceNetErrnoLoc() = 1234; + Require(sceNetSetsockopt(descriptor, 41, 27, &value, sizeof(value)) == 0); + Require(*sceNetErrnoLoc() == 1234); + RequireOption(descriptor, value ? 1 : 0); + } + const int enabled = 1; + Require(setsockopt_nid_postfix(descriptor, 41, 27, &enabled, sizeof(enabled)) == 0); + for (std::uint32_t length = 0; length <= 8; ++length) { + if (length == sizeof(enabled)) continue; + Require(sceNetSetsockopt(descriptor, 41, 27, &enabled, length) == static_cast(0x80410116u) && *sceNetErrnoLoc() == 22); + RequireOption(descriptor, 1); + } + Require(sceNetSetsockopt(descriptor, 41, 27, nullptr, sizeof(enabled)) == static_cast(0x80410116u) && *sceNetErrnoLoc() == 22); + RequireOption(descriptor, 1); + std::uint32_t length = sizeof(enabled); + int output = 0; + Require(sceNetGetsockopt(descriptor, 41, 28, &output, &length) == static_cast(0x8041012au) && *sceNetErrnoLoc() == 42); + Require(sceNetSetsockopt(descriptor, 41, 28, &enabled, sizeof(enabled)) == static_cast(0x8041012au) && *sceNetErrnoLoc() == 42); +} + +static void DualStack(int receiver, int type) { + const int disabled = 0; + Require(sceNetSetsockopt(receiver, 41, 27, &disabled, sizeof(disabled)) == 0); + std::array address{28, 28}; + Require(sceNetBind_nid_postfix(receiver, address.data(), address.size()) == 0); + auto length = static_cast(address.size()); + Require(sceNetGetsockname(receiver, address.data(), &length) == 0); + RequireOption(receiver, 0); + const int enabled = 1; + Require(sceNetSetsockopt(receiver, 41, 27, &enabled, sizeof(enabled)) == static_cast(0x80410116u) && *sceNetErrnoLoc() == 22); + RequireOption(receiver, 0); + std::array destination{16, 2, address[2], address[3], 127, 0, 0, 1}; + const int sender = sceNetSocket("ipv4-peer", 2, type, 0); + Require(sender >= 0); + int connection = receiver; + if (type == 1) { + Require(sceNetListen(receiver, 1) == 0); + Require(sceNetConnect(sender, destination.data(), destination.size()) == 0); + connection = sceNetAccept(receiver, nullptr, nullptr); + Require(connection >= 0); + } + const int timeout = 2000000; + Require(sceNetSetsockopt(connection, 0xffff, 0x1106, &timeout, sizeof(timeout)) == 0); + constexpr std::array payload{'d', 'u', 'a', 'l'}; + if (type == 1) Require(sceNetSend(sender, payload.data(), payload.size(), 0) == payload.size()); + else Require(sceNetSendto(sender, payload.data(), payload.size(), 0, destination.data(), destination.size()) == payload.size()); + std::array received{}; + address.fill(0); + length = address.size(); + Require(sceNetRecvfrom(connection, received.data(), received.size(), 0, address.data(), &length) == received.size()); + Require(received == payload); + if (type == 2) { + Require(length == 28 && address[1] == 28); + Require(address[18] == 0xff && address[19] == 0xff); + Require(address[20] == 127 && address[21] == 0 && address[22] == 0 && address[23] == 1); + } + if (connection != receiver) Require(sceNetSocketClose(connection) == 0); + Require(sceNetSocketClose(sender) == 0); +} + +static void ExerciseGuestOutputs() { + constexpr std::size_t page = 0x4000; + constexpr std::size_t bytes = page * 4; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &physical) == 0); + void* view = nullptr; + void* twin = nullptr; + Require(sceKernelMapDirectMemory(&view, bytes, 3, 0, physical, 0) == 0); + Require(sceKernelMapDirectMemory(&twin, bytes, 3, 0, physical, 0) == 0); + Require(GuestArena::GuestArenaContains_nid_postfix(view, bytes)); + auto* data = static_cast(view); + auto* output = data + page - sizeof(int); + auto* length = reinterpret_cast(data + page * 3 - sizeof(std::uint32_t)); + auto* mirror = static_cast(twin) + page - sizeof(int); + auto* mirrorLength = reinterpret_cast(static_cast(twin) + page * 3 - sizeof(std::uint32_t)); + { + const GuestArena::HostWrite access(view, bytes); + Require(access.Open()); + std::memset(view, 0xa5, bytes); + } + Require(sceKernelMprotect(data + page, page, 1) == 0); + Require(sceKernelMprotect(data + page * 3, page, 1) == 0); + for (const bool net : {true, false}) { + const int descriptor = net ? sceNetSocket("guest-options", 28, 2, 0) : socket_nid_postfix(28, 2, 0); + Require(descriptor >= 0); + int* error = net ? sceNetErrnoLoc() : __error_nid_postfix(); + const auto get = [&](void* value, std::uint32_t* count) { + return net ? sceNetGetsockopt(descriptor, 41, 27, value, count) : + getsockopt_nid_postfix(descriptor, 41, 27, value, count); + }; + const auto reset = [&](std::uint32_t capacity) { + const GuestArena::HostWrite destination(output, sizeof(int)); + const GuestArena::HostWrite lengthOutput(length, sizeof(*length)); + Require(destination.Open() && lengthOutput.Open()); + std::memset(output, 0xa5, sizeof(int)); + *length = capacity; + }; + for (const std::uint32_t capacity : {0u, 1u, 2u, 3u, 4u, 5u, 8u, UINT32_MAX}) { + reset(capacity); +#ifdef _WIN32 + std::array written{}; + std::size_t count = written.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(view), bytes, + written.data(), &count, true)); +#endif + *error = 1234; + Require(get(output, length) == 0 && *error == 1234); + const auto expected = std::min(capacity, sizeof(int)); + const int enabled = 1; + Require(*length == expected && *mirrorLength == expected); + Require(std::memcmp(output, &enabled, expected) == 0 && std::memcmp(mirror, output, sizeof(int)) == 0); + Require(std::all_of(output + expected, output + sizeof(int), [](unsigned char value) { return value == 0xa5; })); + Require(output[-1] == 0xa5 && output[sizeof(int)] == 0xa5); + Require(reinterpret_cast(length)[-1] == 0xa5 && reinterpret_cast(length)[sizeof(*length)] == 0xa5); +#ifdef _WIN32 + count = written.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(view), bytes, + written.data(), &count, true) && count != 0); +#endif + } + reset(0); + Require(get(data + page, length) == 0 && *length == 0 && data[page] == 0xa5); +#ifdef _WIN32 + const int failure = net ? static_cast(0x8041010eu) : -1; + reset(4); + Require(sceKernelMprotect(data, page, 1) == 0); + { + const GuestArena::HostWrite denied(output, sizeof(int)); + Require(!denied.Open()); + } + Require(get(output, length) == failure && *error == 14); + Require(*length == 4 && std::all_of(output, output + sizeof(int), [](unsigned char value) { return value == 0xa5; })); + Require(sceKernelMprotect(data, page, 3) == 0); + reset(8); + Require(sceKernelMprotect(data + page * 2, page, 1) == 0); + { + const GuestArena::HostWrite denied(length, sizeof(*length)); + Require(!denied.Open()); + } + Require(get(output, length) == failure && *error == 14); + Require(*length == 8 && std::all_of(output, output + sizeof(int), [](unsigned char value) { return value == 0xa5; })); + if (!net) Require(get(nullptr, length) == -1 && *error == 14 && *length == 8); + Require(sceKernelMprotect(data + page * 2, page, 3) == 0); +#endif + Require((net ? sceNetSocketClose(descriptor) : close_nid_postfix(descriptor)) == 0); + } + Require(sceKernelMprotect(view, bytes, 3) == 0); + Require(sceKernelMunmap(reinterpret_cast(twin), bytes) == 0); + Require(sceKernelMunmap(reinterpret_cast(view), bytes) == 0); + Require(sceKernelReleaseDirectMemory(physical, bytes) == 0); +} + +int main() { + for (const int type : {1, 2}) { + const int descriptor = sceNetSocket("ipv6-only", 28, type, 0); + if (descriptor < 0 && (*sceNetErrnoLoc() == 43 || *sceNetErrnoLoc() == 47)) return 77; + Require(descriptor >= 0); + Exercise(descriptor); + DualStack(descriptor, type); + Require(sceNetSocketClose(descriptor) == 0); + } + ExerciseGuestOutputs(); + const int ipv4 = sceNetSocket("ipv4-only", 2, 2, 0); + Require(ipv4 >= 0); + const int value = 1; + Require(sceNetSetsockopt(ipv4, 41, 27, &value, sizeof(value)) == static_cast(0x80410116u) && *sceNetErrnoLoc() == 22); + Require(sceNetSocketClose(ipv4) == 0); +} diff --git a/core/libs/prx/libSceNet/tests/Ipv6OnlyPatched.cpp b/core/libs/prx/libSceNet/tests/Ipv6OnlyPatched.cpp new file mode 100644 index 0000000000..c9069ddb09 --- /dev/null +++ b/core/libs/prx/libSceNet/tests/Ipv6OnlyPatched.cpp @@ -0,0 +1,76 @@ +#include "prx/libSceNet/libSceNet.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "Patched IPV6_V6ONLY check failed at line %u\n", location.line()); + std::abort(); +} + +template +static TFunction Function(void* module, const char* name) { + const auto symbol = Nid::ComputeNid(name, "libSceNet"); +#ifdef _WIN32 + auto* address = reinterpret_cast(GetProcAddress(static_cast(module), symbol.c_str())); +#else + auto* address = dlsym(module, symbol.c_str()); +#endif + if (!address) std::fprintf(stderr, "Missing patched function %s (%s)\n", name, symbol.c_str()); + Require(address != nullptr); + return reinterpret_cast(address); +} + +int main(int argc, char** argv) { + Require(argc == 2); + const auto path = std::filesystem::absolute(argv[1]) / "libSceNet.prx"; +#ifdef _WIN32 + SetErrorMode(SEM_FAILCRITICALERRORS); + auto* module = static_cast(LoadLibraryExW(path.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH)); + if (!module) std::fprintf(stderr, "libSceNet.prx: LoadLibraryEx failed with error %lu\n", GetLastError()); +#else + auto* module = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); + if (!module) std::fprintf(stderr, "libSceNet.prx: %s\n", dlerror()); +#endif + Require(module != nullptr); + const auto socket = Function(module, "sceNetSocket"); + const auto set = Function(module, "sceNetSetsockopt"); + const auto get = Function(module, "sceNetGetsockopt"); + const auto close = Function(module, "sceNetSocketClose"); + const auto error = Function(module, "sceNetErrnoLoc"); + for (const int type : {1, 2}) { + const int descriptor = socket("patched-ipv6", 28, type, 0); + if (descriptor < 0 && (*error() == 43 || *error() == 47)) return 77; + Require(descriptor >= 0); + for (const int value : {0, 1, -1, 16}) { + *error() = 1234; + Require(set(descriptor, 41, 27, &value, sizeof(value)) == 0 && *error() == 1234); + const int expected = value != 0 ? 1 : 0; + for (std::uint32_t capacity = 0; capacity <= 8; ++capacity) { + std::array storage; + storage.fill(0xa5); + auto length = capacity; + Require(get(descriptor, 41, 27, storage.data() + 1, &length) == 0 && *error() == 1234); + Require(length == std::min(capacity, sizeof(expected))); + Require(std::memcmp(storage.data() + 1, &expected, length) == 0); + Require(storage.front() == 0xa5 && std::all_of(storage.begin() + 1 + length, storage.end(), [](unsigned char byte) { return byte == 0xa5; })); + } + Require(set(descriptor, 41, 27, &value, sizeof(value) - 1) == static_cast(0x80410116u) && *error() == 22); + int output = -1; + std::uint32_t length = sizeof(output); + Require(get(descriptor, 41, 27, &output, &length) == 0 && output == expected && length == sizeof(output) && *error() == 22); + } + Require(close(descriptor) == 0); + } +} 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 fddd6277ab..525fc51f02 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -1,8 +1,22 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/Ngs2Library.cmake) add_sce_ngs2_library(libSceNgs2.native) +target_sources(libSceNgs2.native PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) +set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp PROPERTIES COMPILE_OPTIONS -fvisibility=default) if(BUILD_TESTING) - foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report Geom VoiceIndex HandleErrors VoiceHandles VoiceEvents) + add_test_executable(ngs2_custom_safety_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2CustomSafety.cpp) + target_include_directories(ngs2_custom_safety_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(ngs2_custom_safety_tests PRIVATE libSceNgs2.native libkernel libc) + configure_windows_unwind(ngs2_custom_safety_tests) + foreach(safetyCase IN ITEMS custom_callback upstream_callback configured_gain routing_one_buffer routing_two_buffers) + add_test(NAME ngs2_custom_safety_${safetyCase} COMMAND ngs2_custom_safety_tests ${safetyCase}) + set_tests_properties(ngs2_custom_safety_${safetyCase} PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(ngs2_custom_safety_${safetyCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Gain Routing 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 +28,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 dfda85d0ae..0d1c8e282b 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -31,6 +31,7 @@ static constexpr int SCE_NGS2_ERROR_INVALID_PAN_PARAM = static_cast(0x804A8 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_PARAM_ID = static_cast(0x804A835A); static constexpr int SCE_NGS2_ERROR_INVALID_GEOM_LISTENER_ADDRESS = static_cast(0x804A8461); static constexpr int SCE_NGS2_ERROR_INVALID_GEOM_FLAG = static_cast(0x804A8463); static constexpr int SCE_NGS2_ERROR_INVALID_OPTION_SIZE = static_cast(0x804A8013); @@ -49,10 +50,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/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index 5d066d889a..2fb63ebcc8 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -89,17 +89,23 @@ int Ngs2CheckCustomRack(std::uint32_t rackId, const Ngs2CustomRackOption& option const int result = CheckBuiltInModule(info); if (result != SCE_NGS2_OK) return result; } - if (option.num_buffers != 1) throw std::runtime_error("NGS2: custom racks with " + std::to_string(option.num_buffers) + " buffers are not implemented"); + if (option.num_buffers != 1 && + (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER || option.num_modules > 1 || + (option.num_modules == 1 && option.module[0].module_id != SCE_NGS2_CUSTOM_MODULE_ID_GAIN))) { + throw std::runtime_error("NGS2: multiple custom buffers require a submixer with at most one Gain module"); + } for (std::uint32_t m = 0; m < option.num_modules; m++) { const auto moduleId = option.module[m].module_id; if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN && option.module[m].option != nullptr) throw std::runtime_error("NGS2: an option for the custom Gain module is not implemented"); - if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN && rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) continue; - if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) throw std::runtime_error("NGS2: the custom Gain module on a custom sampler rack is not implemented"); + if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN && rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) { + throw std::runtime_error("NGS2: custom sampler Gain initialization and lifecycle are not implemented"); + } + if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) continue; const char* name = BuiltInModuleName(moduleId); if (name != nullptr) throw std::runtime_error(std::string("NGS2: the custom ") + name + " module is not implemented"); } for (std::uint32_t p = 0; p < option.rack_option.max_ports; p++) { - if (option.port[p].source_buffer_id != 0) APS5_INVALID_ARG_EX; + if (option.port[p].source_buffer_id >= option.num_buffers) APS5_INVALID_ARG_EX; } return SCE_NGS2_OK; } @@ -111,11 +117,14 @@ static Ngs2UserFx2SetupContext UserFxContext(Ngs2Rack& rack, std::size_t module, } void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option) { + rack.customPortBuffers.resize(option.rack_option.max_ports); + for (std::size_t p = 0; p < rack.customPortBuffers.size(); p++) rack.customPortBuffers[p] = option.port[p].source_buffer_id; rack.customModuleIds.resize(option.num_modules); rack.customModules.resize(option.num_modules); rack.userFx.resize(option.num_modules); rack.userFxCommon.resize(option.num_modules); for (auto& voice : rack.voices) { + voice.customBuffers.resize(option.num_buffers - 1); voice.userFx.resize(option.num_modules); voice.customUserFx.resize(option.num_modules); voice.customGain.assign(option.num_modules, {}); @@ -164,6 +173,8 @@ static void SetupCustomSubmixer(Ngs2Voice& voice, const Ngs2CustomSubmixerVoiceS if (setup.flags != 0) throw std::runtime_error("NGS2: custom submixer setup flags are not implemented"); if (setup.num_input_channels != setup.num_output_channels) throw std::runtime_error("NGS2: custom submixer channel conversion is not implemented"); if (setup.num_input_channels == 0 || setup.num_input_channels > voice.rack->maxChannels) APS5_INVALID_ARG_EX; + for (auto& gain : voice.customGain) if (gain.configured) gain.lifecycleUnknown = true; + ++voice.waveformRevision; voice.channels = setup.num_input_channels; } @@ -188,22 +199,41 @@ static void SetCustomUserFxParam(Ngs2Voice& voice, std::uint32_t index, const Ng fx.flags = 1; } -static void SetGainParam(Ngs2Voice& voice, std::uint32_t index, const Ngs2CustomVoiceGainParam& param) { - if (index >= voice.customGain.size() || voice.rack->customModuleIds[index] != SCE_NGS2_CUSTOM_MODULE_ID_GAIN) APS5_INVALID_ARG_EX; - for (const float gain : param.gain) { - if (!(gain >= -32.0f && gain <= 32.0f)) throw std::runtime_error("NGS2: a custom Gain level outside -32 to 32 is not implemented"); - } +int Ngs2ApplyCustomGainParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& header) { + const auto index = header.id & 0xff; + if (index >= voice.rack->customModuleIds.size()) return SCE_NGS2_ERROR_INVALID_PARAM_ID; + if (voice.rack->customModuleIds[index] != SCE_NGS2_CUSTOM_MODULE_ID_GAIN) return SCE_NGS2_ERROR_INVALID_PARAM_ID; + const auto& param = ParamAs(header); std::copy(std::begin(param.gain), std::end(param.gain), voice.customGain[index].target.begin()); + voice.customGain[index].configured = true; + return SCE_NGS2_OK; } -static void ProcessGain(Ngs2Voice& voice, Ngs2VoiceCustomGain& gain, const std::vector& channels, std::uint32_t grain) { - if (voice.hasSamples) { - for (std::uint32_t c = 0; c < voice.channels; c++) { - const float from = gain.current[c]; - const float step = (gain.target[c] - from) / static_cast(grain); - for (std::uint32_t i = 0; i < grain; i++) channels[c][i] *= from + step * static_cast(i + 1); +static void ProcessGain(Ngs2Voice& voice, std::size_t module, std::uint32_t grain) { + auto& gain = voice.customGain[module]; + if (!gain.configured) throw std::runtime_error("NGS2: custom Gain processing before its first parameter set is not verified"); + if (gain.lifecycleUnknown) throw std::runtime_error("NGS2: custom Gain processing after setup or stop is not verified"); + const auto& info = voice.rack->customModules[module]; + const auto& source = voice.Samples(info.source_buffer_id); + auto& dest = voice.Samples(info.dest_buffer_id); + for (std::uint32_t c = 0; c < voice.channels; c++) { + const auto offset = static_cast(c) * grain; + const float from = gain.current[c]; + const float to = gain.target[c]; + if (from == to) { + if (to == 0.0f) std::fill_n(dest.data() + offset, grain, 0.0f); + else if (to == 1.0f) { + if (source.data() != dest.data()) std::copy_n(source.data() + offset, grain, dest.data() + offset); + } + else for (std::uint32_t i = 0; i < grain; i++) dest[offset + i] = source[offset + i] * to; + } else { + const float step = (to - from) / static_cast(grain); + for (std::uint32_t i = 0; i < grain; i++) dest[offset + i] = source[offset + i] * (from + step * static_cast(i)); } - voice.hasSamples = std::any_of(voice.samples.begin(), voice.samples.end(), [](float sample) { return sample != 0.0f; }); + } + voice.hasSamples = std::any_of(voice.samples.begin(), voice.samples.end(), [](float sample) { return sample != 0.0f; }); + for (const auto& buffer : voice.customBuffers) { + voice.hasSamples |= std::any_of(buffer.begin(), buffer.end(), [](float sample) { return sample != 0.0f; }); } gain.current = gain.target; } @@ -243,11 +273,6 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { Ngs2SetFilter(voice, filter); return; } - if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN) { - if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); - SetGainParam(voice, param.id & 0x1f, ParamAs(param)); - return; - } if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2) { if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SAMPLER && rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) { throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); @@ -266,12 +291,20 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate) { if (voice.userFx.empty() && voice.customUserFx.empty()) return; + const auto revision = voice.waveformRevision; + const auto channelCount = voice.channels; + const auto checkLayout = [&] { + if (voice.waveformRevision != revision || voice.channels != channelCount || voice.samples.size() != static_cast(channelCount) * grain) { + throw std::runtime_error("NGS2: voice setup changes during custom processing are not implemented"); + } + }; + checkLayout(); std::vector channels(voice.channels); for (std::uint32_t c = 0; c < voice.channels; c++) channels[c] = voice.samples.data() + static_cast(c) * grain; for (std::size_t m = 0; m < voice.rack->customModuleIds.size(); m++) { if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_DELAY) continue; if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) { - ProcessGain(voice, voice.customGain[m], channels, grain); + ProcessGain(voice, m, grain); } else if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) { if (!voice.hasSamples) continue; auto& fx = voice.userFx[m]; @@ -280,6 +313,7 @@ void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t samp Ngs2UserFx2ProcessContext context{channels.data(), voice.rack->userFxCommon[m].data(), fx.param.data(), fx.work.data(), fx.state.data(), module.user_data, fx.flags, voice.channels, voice.channels, grain, sampleRate, 0, {}}; const int result = module.process_handler(&context); + checkLayout(); if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx2 process handler failed with " + std::to_string(result)); fx.flags = 0; } else if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) { @@ -288,6 +322,7 @@ void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t samp Ngs2UserFxProcessContext context{channels.data(), fx.data[0], fx.data[1], fx.data[2], fx.flags, voice.channels, grain, sampleRate}; const int result = fx.handler(&context); + checkLayout(); if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx process handler failed with " + std::to_string(result)); fx.flags = 0; voice.hasSamples = std::any_of(voice.samples.begin(), voice.samples.end(), [](float sample) { return sample != 0.0f; }); diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index 73270afab9..b73c150e61 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -93,6 +93,7 @@ struct Ngs2Port { Ngs2Voice* dest = nullptr; float volume = 1.0f; std::int32_t matrix = -1; + std::uint32_t destInput = 0; }; struct Ngs2UserFx2 { @@ -111,6 +112,8 @@ struct Ngs2VoiceCustomUserFx { struct Ngs2VoiceCustomGain { std::array current{}; std::array target{}; + bool configured = false; + bool lifecycleUnknown = false; }; struct Ngs2Voice { @@ -147,6 +150,7 @@ struct Ngs2Voice { std::array userFxData{}; std::uint32_t userFxFlags = 0; std::vector samples; + std::vector> customBuffers; bool rendering = false; bool rendered = false; bool hasSamples = false; @@ -155,6 +159,8 @@ struct Ngs2Voice { void ResetSetup(); const std::uint8_t* WaveformData() const; std::uint32_t InputChannels() const { return inputChannels != 0 ? inputChannels : channels; } + std::vector& Samples(std::uint32_t buffer) { return buffer == 0 ? samples : customBuffers.at(buffer - 1); } + const std::vector& Samples(std::uint32_t buffer) const { return buffer == 0 ? samples : customBuffers.at(buffer - 1); } }; struct Ngs2Rack { @@ -175,6 +181,7 @@ struct Ngs2Rack { std::vector> userFxCommon; std::vector customModuleIds; std::vector customModules; + std::vector customPortBuffers; std::thread::id lockOwner{}; std::uint32_t lockDepth = 0; }; @@ -191,6 +198,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(); @@ -241,6 +250,7 @@ bool Ngs2FinishBlock(Ngs2Voice& voice); int Ngs2CheckCustomRack(std::uint32_t rackId, const Ngs2CustomRackOption& option, std::uint32_t maxInputs); void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option); void Ngs2CleanupUserFx(Ngs2Rack& rack); +int Ngs2ApplyCustomGainParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param); void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param); void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate); void Ngs2SetReverbParams(Ngs2Voice& voice, const Ngs2ReverbI3DL2Param& params); 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..7036b9a377 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()); @@ -219,7 +211,13 @@ float Ngs2DefaultLevel(std::uint32_t sourceChannels, std::uint32_t source, std:: } } -static void MixPort(Ngs2Voice& voice, const Ngs2Voice& source, const Ngs2Port& port, std::uint32_t grain) { +static void MixPort(Ngs2Voice& voice, const Ngs2Voice& source, const Ngs2Port& port, std::uint32_t sourceBuffer, std::uint32_t grain) { + const auto& sourceSamples = source.Samples(sourceBuffer); + auto& destSamples = voice.Samples(port.destInput); + if (sourceSamples.size() != static_cast(grain) * source.channels || + destSamples.size() != static_cast(grain) * voice.InputChannels()) { + throw std::runtime_error("NGS2: channel layout changes during input rendering are not implemented"); + } const auto* matrix = port.matrix < 0 || source.matrices[port.matrix].empty() ? nullptr : &source.matrices[port.matrix]; const auto channels = voice.InputChannels(); const std::size_t outputs = matrix == nullptr ? channels : std::min(channels, matrix->size() / source.channels); @@ -227,18 +225,24 @@ static void MixPort(Ngs2Voice& voice, const Ngs2Voice& source, const Ngs2Port& p for (std::uint32_t src = 0; src < source.channels; src++) { const float level = port.volume * (matrix == nullptr ? Ngs2DefaultLevel(source.channels, src, channels, static_cast(dst)) : (*matrix)[dst * source.channels + src]); if (level == 0.0f) continue; - for (std::uint32_t i = 0; i < grain; i++) voice.samples[dst * grain + i] += source.samples[src * grain + i] * level; + for (std::uint32_t i = 0; i < grain; i++) destSamples[dst * grain + i] += sourceSamples[src * grain + i] * level; } } } static void MixInputs(Ngs2Voice& voice, const std::vector& voices, std::uint32_t grain, std::uint32_t systemRate) { for (auto* source : voices) { - for (const auto& port : source->ports) { + for (std::size_t p = 0; p < source->ports.size(); p++) { + const auto& port = source->ports[p]; if (port.dest != &voice || port.volume == 0.0f) continue; + const auto destInput = port.destInput; RenderVoice(*source, voices, grain, systemRate); + if (port.dest != &voice || port.destInput != destInput) { + throw std::runtime_error("NGS2: patch changes during source rendering are not implemented"); + } if (!source->hasSamples) continue; - MixPort(voice, *source, port, grain); + const auto sourceBuffer = source->rack->customPortBuffers.empty() ? 0u : source->rack->customPortBuffers[p]; + MixPort(voice, *source, port, sourceBuffer, grain); voice.hasSamples = true; } } @@ -253,6 +257,7 @@ static void RenderVoice(Ngs2Voice& voice, const std::vector& voices, ~RenderingScope() { active = false; } } renderingScope{voice.rendering}; voice.samples.assign(static_cast(grain) * voice.InputChannels(), 0.0f); + for (auto& buffer : voice.customBuffers) buffer.assign(static_cast(grain) * voice.InputChannels(), 0.0f); voice.hasSamples = false; if (voice.state == Ngs2PlayState::Playing && voice.channels != 0) { if (voice.rack->rackId == SCE_NGS2_RACK_ID_SAMPLER || voice.rack->rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) { @@ -299,6 +304,7 @@ static void WriteOutput(const Ngs2RenderBufferInfo& output, const std::vector(output.buffer); for (std::size_t i = 0; i < mix.size(); i++) { + if (!std::isfinite(mix[i])) throw std::runtime_error("NGS2: integer rendering of nonfinite samples 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 add38b4d3f..1f61912d77 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -20,6 +20,7 @@ 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; @@ -31,6 +32,10 @@ static void SendEvent(Ngs2Voice& voice, std::uint32_t eventId) { } void Ngs2Voice::SetEvent(std::uint32_t eventId) { + if (state != Ngs2PlayState::Empty && + (eventId == SCE_NGS2_VOICE_EVENT_STOP || eventId == SCE_NGS2_VOICE_EVENT_STOP_IMM || eventId == SCE_NGS2_VOICE_EVENT_KILL)) { + for (auto& gain : customGain) if (gain.configured) gain.lifecycleUnknown = true; + } switch (eventId) { case SCE_NGS2_VOICE_EVENT_PLAY: if (state == Ngs2PlayState::Empty || state == Ngs2PlayState::Stopped) { @@ -58,6 +63,7 @@ void Ngs2Voice::SetEvent(std::uint32_t eventId) { } void Ngs2Voice::ResetSetup() { + for (auto& gain : customGain) if (gain.configured) gain.lifecycleUnknown = true; ++waveformRevision; SetEvent(SCE_NGS2_VOICE_EVENT_STOP_IMM); std::fill(ports.begin(), ports.end(), Ngs2Port{}); @@ -87,7 +93,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 { @@ -96,11 +102,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) { @@ -128,13 +135,15 @@ static void SetPortMatrix(Ngs2Voice& voice, std::uint32_t port, std::int32_t mat static void Patch(Ngs2Voice& voice, const Ngs2VoicePatchParam& patch) { auto& port = PortAt(voice, patch.port); - if (patch.dest_input_id != 0) throw std::runtime_error("NGS2: voice input " + std::to_string(patch.dest_input_id) + " is not implemented"); if (patch.dest_handle == 0) { + if (patch.dest_input_id != 0) throw std::runtime_error("NGS2: a disconnected port with a nonzero input id is not implemented"); port.dest = nullptr; + port.destInput = 0; return; } auto* dest = Ngs2FindVoice(patch.dest_handle); if (dest == nullptr) { + if (patch.dest_input_id != 0) throw std::runtime_error("NGS2: a rack destination with a nonzero input id is not implemented"); auto* rack = Ngs2FindRack(patch.dest_handle); if (rack != nullptr && !rack->voices.empty()) dest = &rack->voices.front(); } @@ -142,7 +151,12 @@ static void Patch(Ngs2Voice& voice, const Ngs2VoicePatchParam& patch) { dest->rack->rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) { APS5_INVALID_ARG_EX; } + if (patch.dest_input_id != 0 && + (dest->rack->rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER || patch.dest_input_id >= dest->rack->maxInputs)) { + throw std::runtime_error("NGS2: voice input " + std::to_string(patch.dest_input_id) + " is not implemented for this rack"); + } port.dest = dest; + port.destInput = patch.dest_input_id; } static void ApplyCommonParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { @@ -187,8 +201,9 @@ static void SetupSampler(Ngs2Voice& voice, const Ngs2WaveformFormat& format) { return; } voice.ResetSetup(); - 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; @@ -239,7 +254,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; @@ -251,7 +266,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; } @@ -392,9 +407,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) { @@ -419,7 +433,12 @@ int APS5_VABI sceNgs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamHe auto& voice = *found; try { for (const auto* param = param_list;; param = reinterpret_cast(reinterpret_cast(param) + param->next)) { - ApplyParam(voice, *param); + if ((param->id & ~0xffu) == SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN) { + const auto result = Ngs2ApplyCustomGainParam(voice, *param); + if (result != SCE_NGS2_OK) return result; + } else { + ApplyParam(voice, *param); + } if (param->next == 0) break; } } catch (const Ngs2ReturnCode& result) { @@ -519,6 +538,7 @@ int APS5_VABI sceNgs2VoiceGetPortInfo(uintptr_t voice_handle, uint32_t port, Ngs info->matrix_id = source.matrix; info->volume = source.volume; info->dest_handle = reinterpret_cast(source.dest); + info->dest_input_id = source.destInput; return SCE_NGS2_OK; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index 47a189ed42..4e8f3999b7 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -521,8 +521,6 @@ static void TestCustomRackChecks() { Require(rack == 0); sampler.custom_rack_option.module[0].extra_buffer_id = 0; Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &sampler.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK); - sampler.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_GAIN; - Require(CreateSamplerThrows(sampler)); Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); for (const auto count : {1u, 2u, 24u}) { sampler.custom_rack_option.num_modules = count; @@ -757,17 +755,14 @@ static void TestCustomGain() { Patch(sampler, custom); Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); - auto out = Render(system); - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.0f && out[i * 2 + 1] == 0.0f); - Ngs2CustomVoiceGainParam gain{}; gain.gain[0] = 2.0f; gain.gain[1] = -0.5f; Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, gain); - out = Render(system); + auto out = Render(system); for (std::uint32_t i = 0; i < Grain; i++) { - Require(out[i * 2] == 0.5f * 2.0f * static_cast(i + 1) / Grain); - Require(out[i * 2 + 1] == 0.5f * -0.5f * static_cast(i + 1) / Grain); + Require(out[i * 2] == 0.5f * 2.0f * static_cast(i) / Grain); + Require(out[i * 2 + 1] == 0.5f * -0.5f * static_cast(i) / Grain); } out = Render(system); for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 1.0f && out[i * 2 + 1] == -0.25f); @@ -775,13 +770,15 @@ static void TestCustomGain() { gain.gain[0] = 1.0f; Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, gain); out = Render(system); - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f * (2.0f - static_cast(i + 1) / Grain)); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f * (2.0f - static_cast(i) / Grain)); auto outside = gain; outside.gain[2] = 33.0f; - Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, outside)); - Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 1, gain)); - Require(ControlThrows(sampler, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, gain)); + Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, outside); + gain.header = {sizeof(gain), 0, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 1}; + Require(sceNgs2VoiceControl(custom, &gain.header) == SCE_NGS2_ERROR_INVALID_PARAM_ID); + gain.header.id = SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN; + Require(sceNgs2VoiceControl(sampler, &gain.header) == SCE_NGS2_ERROR_INVALID_PARAM_ID); struct ShortGainParam { Ngs2VoiceParamHeader header; float gain[2]; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2CustomSafety.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2CustomSafety.cpp new file mode 100644 index 0000000000..6e0475ad98 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2CustomSafety.cpp @@ -0,0 +1,147 @@ +#include "prx/libSceNgs2.native/tests/Ngs2Test.hpp" + +#include +#include +#include + +static std::uint32_t processCalls = 0; + +static int APS5_VABI Generate(Ngs2UserFxProcessContext* context) { + Require(++processCalls == 1); + std::fill_n(context->channel_data[0], context->num_grain_samples, 1.0f); + std::fill_n(context->channel_data[1], context->num_grain_samples, -0.5f); + return 0; +} + +static uintptr_t Custom(uintptr_t system, std::uint32_t count, bool chain, std::uint32_t outputBuffer = 0) { + Ngs2CustomSubmixerRackOption option{}; + auto& custom = option.custom_rack_option; + custom.rack_option.size = sizeof(option); + custom.rack_option.max_grain_samples = 512; + custom.rack_option.max_voices = 1; + custom.rack_option.max_ports = 1; + custom.num_buffers = count; + custom.port[0].source_buffer_id = outputBuffer; + custom.num_modules = chain ? 2 : 0; + custom.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_USER_FX; + custom.module[1].module_id = SCE_NGS2_CUSTOM_MODULE_ID_GAIN; + option.max_channels = 8; + option.max_inputs = count; + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &custom.rack_option, &query) == 0); + const auto storage = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &custom.rack_option, &storage, &rack) == 0); + const auto voice = Voice(rack); + Control(voice, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0}); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + return voice; +} + +static void ExpectOutput(uintptr_t system, float left, float right) { + std::array out{}; + const Ngs2RenderBufferInfo info{out.data(), sizeof(out), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}; + Require(sceNgs2SystemRender(system, &info, 1) == 0); + for (std::uint32_t i = 0; i < Grain; i++) { + Require(out[i * 2] == left && out[i * 2 + 1] == right); + } +} + +static void TestCustomCallback() { + processCalls = 0; + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + const auto target = Custom(system, 1, true); + Patch(target, master); + Control(target, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX, Ngs2CustomVoiceUserFxParam{{}, Generate, 0, 0, 0}); + Control(target, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 1, Ngs2CustomVoiceGainParam{}); + ExpectOutput(system, 0.0f, 0.0f); + Require(processCalls == 1); + Require(sceNgs2SystemDestroy(system, nullptr) == 0); +} + +static void TestUpstreamCallback() { + processCalls = 0; + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + const auto target = Custom(system, 2, false, 1); + Patch(target, master); + const auto source = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER)); + Control(source, SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP, Ngs2SubmixerVoiceSetupParam{{}, 2, 0}); + Control(source, SCE_NGS2_SUBMIXER_VOICE_PARAM_USER_FX, Ngs2SubmixerVoiceUserFxParam{{}, Generate, 0, 0, 0}); + Control(source, SCE_NGS2_VOICE_PARAM_PATCH, Ngs2VoicePatchParam{{}, 0, 1, target}); + Event(source, SCE_NGS2_VOICE_EVENT_PLAY); + ExpectOutput(system, 1.0f, -0.5f); + Require(processCalls == 1); + Require(sceNgs2SystemDestroy(system, nullptr) == 0); +} + +static void TestConfiguredGain() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + const auto target = Custom(system, 1, true); + Patch(target, master); + Control(target, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 1, Ngs2CustomVoiceGainParam{}); + ExpectOutput(system, 0.0f, 0.0f); + Require(sceNgs2SystemDestroy(system, nullptr) == 0); +} + +struct CallbackContext { + std::uint32_t calls = 0; +}; + +static void APS5_VABI CountBlockEnd(const Ngs2VoiceCallbackInfo* info) { + auto& context = *reinterpret_cast(info->callback_data); + Require(++context.calls == 1); +} + +static void TestRouting(std::uint32_t oldBuffers) { + const auto system = CreateSystem(); + const auto firstMaster = Mastering(system, 2); + const auto secondMaster = Mastering(system, 2); + Control(secondMaster, SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT, Ngs2MasteringVoiceOutputParam{{}, 1, 0}); + const auto first = Custom(system, oldBuffers, false, oldBuffers - 1); + const auto second = Custom(system, 2, false, 1); + Patch(first, firstMaster); + Patch(second, secondMaster); + const auto source = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); + Control(source, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, + Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 48000, 0, 0, 0}}); + std::array pcm{}; + pcm.fill(8192); + const Ngs2WaveformBlock block{0, sizeof(pcm), 0, 0, Grain, 0, 0}; + Control(source, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, + Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); + CallbackContext context; + Control(source, SCE_NGS2_VOICE_PARAM_CALLBACK, + Ngs2VoiceCallbackParam{{}, CountBlockEnd, reinterpret_cast(&context), SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + Patch(source, first); + Control(source, SCE_NGS2_VOICE_PARAM_PATCH, Ngs2VoicePatchParam{{}, 0, 1, second}); + Event(source, SCE_NGS2_VOICE_EVENT_PLAY); + std::array, 2> outputs{}; + std::array info{}; + for (std::size_t i = 0; i < outputs.size(); i++) { + outputs[i].fill(9.0f); + info[i] = {outputs[i].data(), sizeof(outputs[i]), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}; + } + Require(sceNgs2SystemRender(system, info.data(), info.size()) == SCE_NGS2_OK); + Require(context.calls == 1); + Ngs2VoicePortInfo port{}; + Require(sceNgs2VoiceGetPortInfo(source, 0, &port, sizeof(port)) == SCE_NGS2_OK); + Require(port.dest_handle == second && port.dest_input_id == 1); + for (const auto sample : outputs[0]) Require(sample == 0.0f); + for (const auto sample : outputs[1]) Require(sample == 0.25f); + Require(sceNgs2SystemDestroy(system, nullptr) == 0); +} + +int main(int argc, char** argv) { + Require(argc == 2); + const std::string_view test = argv[1]; + if (test == "custom_callback") TestCustomCallback(); + else if (test == "upstream_callback") TestUpstreamCallback(); + else if (test == "configured_gain") TestConfiguredGain(); + else if (test == "routing_one_buffer") TestRouting(1); + else if (test == "routing_two_buffers") TestRouting(2); + else return 1; + return 0; +} diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Gain.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Gain.cpp new file mode 100644 index 0000000000..12b15b252b --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Gain.cpp @@ -0,0 +1,226 @@ +#include "Ngs2Test.hpp" + +#include +#include +#include +#include +#include + +static std::int32_t APS5_VABI Generate(Ngs2UserFxProcessContext* context) { + const auto* signal = reinterpret_cast(context->user_data0); + for (std::uint32_t c = 0; c < context->num_channels; c++) + std::fill_n(context->channel_data[c], context->num_grain_samples, signal[c]); + return SCE_NGS2_OK; +} + +struct GainGraph { + std::uint32_t channels; + uintptr_t system = CreateSystem(); + std::array voices{}; + std::array signal{0.5f, -0.25f, 0.125f, -0.0625f, 0.03125f, -0.015625f, 0.0078125f, -0.00390625f}; + + GainGraph(std::uint32_t channelCount = 2, std::uint32_t voiceCount = 1) + : channels(channelCount) { + const auto master = Mastering(system, channels); + Ngs2CustomSubmixerRackOption mixerOption{}; + auto& custom = mixerOption.custom_rack_option; + custom.rack_option.size = sizeof(mixerOption); + custom.rack_option.max_grain_samples = 512; + custom.rack_option.max_voices = voiceCount; + custom.rack_option.max_matrices = 1; + custom.rack_option.max_ports = 1; + custom.num_buffers = 1; + custom.num_modules = 1; + custom.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_GAIN; + mixerOption.max_channels = channels; + mixerOption.max_inputs = 1; + const auto rackId = SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER; + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(rackId, &custom.rack_option, &query) == SCE_NGS2_OK); + const auto buffer = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, rackId, &custom.rack_option, &buffer, &rack) == SCE_NGS2_OK); + for (std::uint32_t v = 0; v < voiceCount; v++) { + Require(sceNgs2RackGetVoiceHandle(rack, v, &voices[v]) == SCE_NGS2_OK); + Control(voices[v], SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, + Ngs2CustomSubmixerVoiceSetupParam{{}, channels, channels, 0, 0}); + const auto source = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER)); + Control(source, SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP, Ngs2SubmixerVoiceSetupParam{{}, channels, 0}); + Control(source, SCE_NGS2_SUBMIXER_VOICE_PARAM_USER_FX, + Ngs2SubmixerVoiceUserFxParam{{}, Generate, reinterpret_cast(signal.data()), 0, 0}); + Patch(source, voices[v]); + Event(source, SCE_NGS2_VOICE_EVENT_PLAY); + Patch(voices[v], master); + Event(voices[v], SCE_NGS2_VOICE_EVENT_PLAY); + } + } + + ~GainGraph() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } + + void Set(std::initializer_list values, std::uint32_t voice = 0) { + Ngs2CustomVoiceGainParam param{}; + std::fill_n(param.gain, 37, std::numeric_limits::quiet_NaN()); + std::copy(values.begin(), values.end(), param.gain); + Control(voices[voice], SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN, param); + } + + std::vector Render(std::uint32_t grain = Grain) { + Require(sceNgs2SystemSetGrainSamples(system, grain) == SCE_NGS2_OK); + std::vector out(grain * channels, -123.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; + } +}; + +static void ExpectChannels(const std::vector& out, std::initializer_list> channels) { + std::size_t c = 0; + for (const auto& samples : channels) { + Require(out.size() == samples.size() * channels.size()); + for (std::size_t i = 0; i < samples.size(); i++) Require(out[i * channels.size() + c] == samples[i]); + c++; + } +} + +static void ExpectFrame(const std::vector& out, std::size_t index, std::initializer_list frame) { + Require((index + 1) * frame.size() <= out.size()); + Require(std::equal(frame.begin(), frame.end(), out.begin() + index * frame.size())); +} + +static void ExpectConstant(const std::vector& out, std::initializer_list frame) { + Require(out.size() % frame.size() == 0); + for (std::size_t i = 0; i < out.size() / frame.size(); i++) ExpectFrame(out, i, frame); +} + +static void TestRamps() { + GainGraph graph; + graph.Set({64.0f, -64.0f}); + graph.Set({8.0f, -16.0f}); + ExpectChannels(graph.Render(), {{0, 0.5f, 1, 1.5f, 2, 2.5f, 3, 3.5f}, {0, 0.5f, 1, 1.5f, 2, 2.5f, 3, 3.5f}}); + ExpectConstant(graph.Render(), {4, 4}); + graph.Set({8.0f, -16.0f}); + ExpectConstant(graph.Render(), {4, 4}); + graph.Set({16.0f, 16.0f}); + graph.Set({-80.0f, 96.0f}); + ExpectChannels(graph.Render(), {{4, -1.5f, -7, -12.5f, -18, -23.5f, -29, -34.5f}, {4, 0.5f, -3, -6.5f, -10, -13.5f, -17, -20.5f}}); + ExpectConstant(graph.Render(), {-40, -24}); + graph.Set({0, 0}); + ExpectChannels(graph.Render(), {{-40, -35, -30, -25, -20, -15, -10, -5}, {-24, -21, -18, -15, -12, -9, -6, -3}}); + ExpectConstant(graph.Render(), {0, 0}); +} + +static void TestEightChannels() { + GainGraph graph(8); + graph.Set({2, 8, 24, 64, 160, 384, 896, 2048}); + const auto out = graph.Render(); + ExpectFrame(out, 0, {0, 0, 0, 0, 0, 0, 0, 0}); + ExpectFrame(out, 4, {0.5f, -1, 1.5f, -2, 2.5f, -3, 3.5f, -4}); + ExpectFrame(out, 7, {7.0f / 8, -7.0f / 4, 21.0f / 8, -7.0f / 2, 35.0f / 8, -21.0f / 4, 49.0f / 8, -7}); + ExpectConstant(graph.Render(), {1, -2, 3, -4, 5, -6, 7, -8}); +} + +static void TestVoicesAndPause() { + GainGraph graph(1, 2); + graph.Set({8}, 0); + graph.Set({-4}, 1); + ExpectChannels(graph.Render(), {{0, 0.25f, 0.5f, 0.75f, 1, 1.25f, 1.5f, 1.75f}}); + ExpectConstant(graph.Render(), {2}); + Event(graph.voices[0], SCE_NGS2_VOICE_EVENT_PAUSE); + graph.Set({16}, 0); + ExpectConstant(graph.Render(), {-2}); + Event(graph.voices[0], SCE_NGS2_VOICE_EVENT_RESUME); + ExpectChannels(graph.Render(), {{2, 2.5f, 3, 3.5f, 4, 4.5f, 5, 5.5f}}); + ExpectConstant(graph.Render(), {6}); + graph.Set({-12}, 1); + ExpectChannels(graph.Render(), {{6, 5.5f, 5, 4.5f, 4, 3.5f, 3, 2.5f}}); + ExpectConstant(graph.Render(), {2}); +} + +static void TestGrainChanges() { + GainGraph graph(1); + graph.Set({8}); + auto out = graph.Render(64); + Require(out.front() == 0 && out[1] == 1.0f / 16 && out[32] == 2 && out.back() == 63.0f / 16); + ExpectConstant(graph.Render(64), {4}); + graph.Set({-8}); + out = graph.Render(128); + Require(out.front() == 4 && out[1] == 63.0f / 16 && out[64] == 0 && out.back() == -63.0f / 16); + ExpectConstant(graph.Render(), {-4}); +} + +static void TestNonfiniteGains() { + GainGraph graph(1); + graph.Set({std::numeric_limits::infinity()}); + graph.Render(); + graph.Set({std::numeric_limits::infinity()}); + for (float sample : graph.Render()) Require(std::isinf(sample) && sample > 0); + graph.Set({std::numeric_limits::quiet_NaN()}); + for (float sample : graph.Render()) Require(std::isnan(sample)); + for (float sample : graph.Render()) Require(std::isnan(sample)); + graph.Set({2}); + for (float sample : graph.Render()) Require(std::isnan(sample)); + ExpectConstant(graph.Render(), {1}); +} + +static void TestZeroClearsNanSource() { + GainGraph graph(1); + graph.signal[0] = std::numeric_limits::quiet_NaN(); + graph.Set({1}); + for (float sample : graph.Render()) Require(std::isnan(sample)); + graph.Set({0}); + for (float sample : graph.Render()) Require(std::isnan(sample)); + ExpectConstant(graph.Render(), {0}); +} + +static void TestParameterPositions() { + GainGraph graph(1); + graph.Set({8}); + Ngs2CustomVoiceGainParam param{}; + param.header = {sizeof(param), 0, 0}; + param.gain[0] = 16; + for (std::uint32_t position = 0; position < 256; position++) { + if (position == 0) continue; + param.header.id = SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | position; + Require(sceNgs2VoiceControl(graph.voices[0], ¶m.header) == SCE_NGS2_ERROR_INVALID_PARAM_ID); + } + ExpectChannels(graph.Render(), {{0, 0.5f, 1, 1.5f, 2, 2.5f, 3, 3.5f}}); + ExpectConstant(graph.Render(), {4}); +} + +static void TestLinkedParameters() { + GainGraph graph(1); + std::array params{}; + for (auto& param : params) param.header = {sizeof(param), sizeof(param), SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN}; + params[0].gain[0] = 8; + params[1].header.id |= 255; + params[2].gain[0] = 16; + params[2].header.next = 0; + Require(sceNgs2VoiceControl(graph.voices[0], ¶ms[0].header) == SCE_NGS2_ERROR_INVALID_PARAM_ID); + ExpectChannels(graph.Render(), {{0, 0.5f, 1, 1.5f, 2, 2.5f, 3, 3.5f}}); + ExpectConstant(graph.Render(), {4}); +} + +static void TestIntegerOutput() { + for (const float gain : {1.0f, -1.0f}) { + GainGraph graph(1); + graph.Set({gain}); + graph.Render(); + std::array out{}; + const Ngs2RenderBufferInfo info{out.data(), sizeof(out), SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1}; + Require(sceNgs2SystemRender(graph.system, &info, 1) == SCE_NGS2_OK); + for (const auto sample : out) Require(sample == (gain > 0.0f ? 16384 : -16384)); + } +} + +int main() { + TestRamps(); + TestEightChannels(); + TestVoicesAndPause(); + TestGrainChanges(); + TestNonfiniteGains(); + TestParameterPositions(); + TestZeroClearsNanSource(); + TestLinkedParameters(); + TestIntegerOutput(); + return 0; +} diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp index 9682af0e24..817367de68 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp @@ -1,4 +1,5 @@ #include "Ngs2Test.hpp" +#include "prx/libSceNgs2.native/include/Api.hpp" #include #include @@ -13,6 +14,11 @@ static void Status(int actual, int expected, const char* message) { if (actual != expected) throw std::runtime_error(message); } +static void CheckExportLinked() { + auto* volatile linked = &sceNgs2RackQueryInfo; + Require(linked != nullptr); +} + static void CheckResetOption() { Status(sceNgs2SystemResetOption(nullptr), static_cast(0x804A8010u), "null system option did not return the invalid out address error"); constexpr std::uint64_t size = 0x90; @@ -316,6 +322,7 @@ int main() { CheckSystemOption(); CheckSetGrainSamples(); CheckSetSampleRate(); + CheckExportLinked(); std::cout << "NGS2 PS5 handle errors and validation precedence checked\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceNgs2.native/tests/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 3f8ad014ad..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 @@ -1008,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); @@ -1044,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); @@ -1729,6 +1801,9 @@ int main() { TestReverbSetup(); TestReverbConversion(); TestSampleRate(); + + TestMasteringGainClampedLikePs5(); + TestSystemOptionFlags(); TestUserData(); TestPcmCallbackReset(); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Routing.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Routing.cpp new file mode 100644 index 0000000000..86789ec1a4 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Routing.cpp @@ -0,0 +1,233 @@ +#include "prx/libSceNgs2.native/tests/Ngs2Test.hpp" + +#include +#include +#include +#include + +static Ngs2CustomSubmixerRackOption RackOption(std::uint32_t count, std::uint32_t channels = 2) { + Ngs2CustomSubmixerRackOption option{}; + auto& custom = option.custom_rack_option; + custom.rack_option.size = sizeof(option); + custom.rack_option.max_grain_samples = 512; + custom.rack_option.max_voices = 2; + custom.rack_option.max_matrices = 1; + custom.rack_option.max_ports = count; + custom.num_buffers = count; + for (std::uint32_t port = 0; port < count; port++) custom.port[port].source_buffer_id = port; + option.max_channels = channels; + option.max_inputs = count == 1 ? 1 : 2; + return option; +} + +static void AddGain(Ngs2CustomSubmixerRackOption& option, std::uint32_t source, std::uint32_t dest) { + auto& custom = option.custom_rack_option; + custom.num_modules = 1; + custom.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_GAIN; + custom.module[0].source_buffer_id = source; + custom.module[0].dest_buffer_id = dest; + custom.module[0].extra_buffer_id = custom.num_buffers == 1 ? 0 : 1; +} + +static uintptr_t CustomRack(uintptr_t system, const Ngs2CustomSubmixerRackOption& option) { + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &option.custom_rack_option.rack_option, &query) == SCE_NGS2_OK); + const auto info = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &option.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK); + return rack; +} + +static uintptr_t CustomVoice(uintptr_t rack, std::uint32_t index = 0, std::uint32_t channels = 2) { + uintptr_t voice = 0; + Require(sceNgs2RackGetVoiceHandle(rack, index, &voice) == SCE_NGS2_OK); + Control(voice, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, channels, channels, 0, 0}); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + return voice; +} + +static void SetGain(uintptr_t voice, float level) { + Ngs2CustomVoiceGainParam param{}; + for (auto& gain : param.gain) gain = level; + Control(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN, param); +} + +static void Route(uintptr_t source, std::uint32_t port, uintptr_t dest, std::uint32_t input = 0, std::uint32_t channels = 2) { + std::array identity{}; + for (std::uint32_t channel = 0; channel < channels; channel++) identity[channel * channels + channel] = 1.0f; + Control(source, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 0, channels * channels, identity.data()}); + Control(source, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, port, 0}); + Control(source, SCE_NGS2_VOICE_PARAM_PATCH, Ngs2VoicePatchParam{{}, port, input, dest}); + Ngs2VoicePortInfo info{}; + Require(sceNgs2VoiceGetPortInfo(source, port, &info, sizeof(info)) == SCE_NGS2_OK); + Require(info.dest_handle == dest && info.dest_input_id == input); +} + +static void Output(uintptr_t system, uintptr_t source, std::uint32_t port, std::uint32_t output, std::uint32_t channels = 2) { + const auto master = Mastering(system, channels); + Control(master, SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT, Ngs2MasteringVoiceOutputParam{{}, output, 0}); + Route(source, port, master, 0, channels); +} + +static uintptr_t Input(uintptr_t system, const std::vector& pcm, std::uint32_t channels = 2) { + const auto voice = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, + Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, channels, 48000, 0, 0, 0}}); + const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 0, 0, static_cast(pcm.size() / channels), 0, 0}; + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + return voice; +} + +static void Render(uintptr_t system, const std::vector& expected, std::uint32_t channels = 2, std::uint32_t grain = Grain) { + Require(sceNgs2SystemSetGrainSamples(system, grain) == SCE_NGS2_OK); + std::vector> samples(expected.size(), std::vector(grain * channels, 9.0f)); + std::vector info; + for (auto& output : samples) info.push_back({output.data(), output.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, channels}); + Require(sceNgs2SystemRender(system, info.data(), static_cast(info.size())) == SCE_NGS2_OK); + for (std::size_t port = 0; port < expected.size(); port++) { + for (const auto sample : samples[port]) Require(std::fabs(sample - expected[port]) <= 0.000001f); + } +} + +static void TestNoModules(std::uint32_t count, std::vector expected) { + const auto system = CreateSystem(); + auto option = RackOption(count); + for (std::uint32_t port = 0; port < count; port++) option.custom_rack_option.port[port].source_buffer_id = count - port - 1; + const auto custom = CustomVoice(CustomRack(system, option)); + for (std::uint32_t port = 0; port < count; port++) Output(system, custom, port, port); + const std::vector pcm0(Grain * 2 * 16, 8192); + const std::vector pcm1(Grain * 2 * 16, -24576); + const auto input0 = Input(system, pcm0); + Route(input0, 0, custom); + uintptr_t input1 = 0; + if (count > 1) { + input1 = Input(system, pcm1); + Route(input1, 0, custom, 1); + } + Render(system, expected); + if (count > 1) { + Route(input1, 0, custom, 0); + expected[count - 1] = -0.5f; + expected[count - 2] = 0.0f; + Render(system, expected); + Route(input1, 0, 0); + expected[count - 1] = 0.25f; + Render(system, expected); + } + Route(input0, 0, 0); + Render(system, std::vector(count, 0.0f)); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestGainRouting() { + struct Case { + std::uint32_t count, source, dest; + std::vector expected; + }; + const Case cases[] = { + {1, 0, 0, {0.5f}}, + {2, 0, 1, {0.25f, 0.5f}}, + {2, 1, 0, {-1.5f, -0.75f}}, + {3, 0, 2, {0.25f, -0.75f, 0.5f}}, + {3, 2, 1, {0.25f, 0.0f, 0.0f}}, + {4, 1, 3, {0.25f, -0.75f, 0.0f, -1.5f}}, + {4, 3, 0, {0.0f, -0.75f, 0.0f, 0.0f}}, + {4, 1, 1, {0.25f, -1.5f, 0.0f, 0.0f}}, + }; + for (const auto& test : cases) { + const auto system = CreateSystem(); + auto option = RackOption(test.count); + AddGain(option, test.source, test.dest); + const auto custom = CustomVoice(CustomRack(system, option)); + SetGain(custom, 2.0f); + for (std::uint32_t port = 0; port < test.count; port++) Output(system, custom, port, port); + const std::vector silence(test.count, 0.0f); + Render(system, silence); + const std::vector pcm0(Grain * 2 * 16, 8192); + const std::vector pcm1(Grain * 2 * 16, -24576); + const auto input0 = Input(system, pcm0); + Route(input0, 0, custom); + uintptr_t input1 = 0; + if (test.count > 1) { + input1 = Input(system, pcm1); + Route(input1, 0, custom, 1); + } + Render(system, test.expected); + Render(system, test.expected, 2, 64); + Route(input0, 0, 0); + if (input1 != 0) Route(input1, 0, 0); + Render(system, silence); + Render(system, silence); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + } +} + +static void TestUnityCopy() { + const auto system = CreateSystem(); + auto option = RackOption(2); + AddGain(option, 0, 1); + const auto custom = CustomVoice(CustomRack(system, option)); + SetGain(custom, 1.0f); + Output(system, custom, 0, 0); + Output(system, custom, 1, 1); + Render(system, {0.0f, 0.0f}); + std::vector source(Grain * 2), dest(Grain * 2); + for (std::uint32_t i = 0; i < Grain; i++) { + source[i * 2] = 8192; + source[i * 2 + 1] = -16384; + dest[i * 2] = -24576; + dest[i * 2 + 1] = 4096; + } + Route(Input(system, source), 0, custom); + Route(Input(system, dest), 0, custom, 1); + std::array, 2> outputs{}; + std::array info{}; + for (std::size_t i = 0; i < outputs.size(); i++) { + info[i] = {outputs[i].data(), sizeof(outputs[i]), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}; + } + Require(sceNgs2SystemRender(system, info.data(), info.size()) == SCE_NGS2_OK); + for (const auto& output : outputs) { + for (std::uint32_t i = 0; i < Grain; i++) Require(output[i * 2] == 0.25f && output[i * 2 + 1] == -0.5f); + } + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestIndependentVoicesAndSilentRamp(std::uint32_t channels) { + const auto system = CreateSystem(); + auto option = RackOption(4, channels); + AddGain(option, 0, 3); + const auto rack = CustomRack(system, option); + const auto first = CustomVoice(rack, 0, channels); + const auto second = CustomVoice(rack, 1, channels); + SetGain(first, 2.0f); + SetGain(second, -1.0f); + Output(system, first, 3, 0, channels); + Output(system, second, 3, 1, channels); + Render(system, {0.0f, 0.0f}, channels); + const std::vector pcm0(Grain * channels * 16, 8192); + const std::vector pcm1(Grain * channels * 16, -24576); + const auto input0 = Input(system, pcm0, channels); + const auto input1 = Input(system, pcm1, channels); + Route(input0, 0, first, 0, channels); + Route(input1, 0, second, 0, channels); + Render(system, {0.5f, 0.75f}, channels); + Route(input0, 0, 0, 0, channels); + Render(system, {0.0f, 0.75f}, channels); + SetGain(first, 4.0f); + Render(system, {0.0f, 0.75f}, channels); + Route(input0, 0, first, 0, channels); + Render(system, {1.0f, 0.75f}, channels); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +int main() { + TestNoModules(1, {0.25f}); + TestNoModules(2, {-0.75f, 0.25f}); + TestNoModules(3, {0.0f, -0.75f, 0.25f}); + TestNoModules(4, {0.0f, 0.0f, -0.75f, 0.25f}); + TestGainRouting(); + TestUnityCopy(); + for (std::uint32_t channels = 1; channels <= 8; channels++) TestIndependentVoicesAndSilentRamp(channels); + return 0; +} diff --git a/core/libs/prx/libSceNgs2/CMakeLists.txt b/core/libs/prx/libSceNgs2/CMakeLists.txt index 41b6cfe96a..d64e678c3e 100644 --- a/core/libs/prx/libSceNgs2/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2/CMakeLists.txt @@ -1,8 +1,33 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/Ngs2Library.cmake) add_sce_ngs2_library(libSceNgs2) target_sources(libSceNgs2 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/PendingExports.cpp) +set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/PendingExports.cpp PROPERTIES COMPILE_OPTIONS -fvisibility=default) if(BUILD_TESTING) + add_test_executable(ngs2_core_custom_safety_tests ${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/tests/Ngs2CustomSafety.cpp) + target_include_directories(ngs2_core_custom_safety_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(ngs2_core_custom_safety_tests PRIVATE libSceNgs2 libkernel libc) + configure_windows_unwind(ngs2_core_custom_safety_tests) + foreach(safetyCase IN ITEMS custom_callback upstream_callback configured_gain routing_one_buffer routing_two_buffers) + add_test(NAME ngs2_core_custom_safety_${safetyCase} COMMAND ngs2_core_custom_safety_tests ${safetyCase}) + set_tests_properties(ngs2_core_custom_safety_${safetyCase} PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(ngs2_core_custom_safety_${safetyCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + foreach(ngs2Test IN ITEMS Gain Routing) + string(TOLOWER ${ngs2Test} testName) + add_test_executable(ngs2_core_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/tests/Ngs2${ngs2Test}.cpp) + target_include_directories(ngs2_core_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) + target_link_libraries(ngs2_core_${testName}_tests PRIVATE libSceNgs2 atrac9 libkernel libc) + configure_windows_unwind(ngs2_core_${testName}_tests) + add_test(NAME ngs2_core_${testName} COMMAND ngs2_core_${testName}_tests) + if(WIN32) + set_tests_properties(ngs2_core_${testName} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test_executable(ngs2_core_render_tests ${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/tests/Ngs2Render.cpp) target_include_directories(ngs2_core_render_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) target_link_libraries(ngs2_core_render_tests PRIVATE libSceNgs2 atrac9 libkernel libc) @@ -12,4 +37,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/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp index 6ab09331ae..de171a79a9 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; } @@ -296,6 +335,12 @@ static void ReleaseIntrusivePtr(IntrusivePtr* self, const std::size_t referenceC self->deleter = nullptr; } +static void AssignIntrusivePtr(IntrusivePtr* self, const IntrusivePtr* source, const std::size_t referenceCountOffset, const char* function) { + if (self->object == source->object) return; + ReleaseIntrusivePtr(self, referenceCountOffset, function); + CopyIntrusivePtr(self, source, referenceCountOffset); +} + void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { CopyIntrusivePtr(self, source, ReferenceCountOffset); } @@ -1721,28 +1766,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 +1822,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; } @@ -2412,8 +2452,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V113PlayerSessionEEaSERS7_(IntrusivePtr* self, const IntrusivePtr* source) { - ReleaseIntrusivePtr(self, 0x00, __func__); - CopyIntrusivePtr(self, source, 0x00); + AssignIntrusivePtr(self, source, 0x00, __func__); } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V12ToEEC1Ev(IntrusivePtr* self) { @@ -2687,8 +2726,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V1 } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V113PlayerSessionEEEEEEaSERKSA_(IntrusivePtr* self, const IntrusivePtr* source) { - ReleaseIntrusivePtr(self, 0x30, __func__); - CopyIntrusivePtr(self, source, 0x30); + AssignIntrusivePtr(self, source, 0x30, __func__); } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V12ToEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { @@ -3014,8 +3052,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() { @@ -3143,8 +3181,7 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGam } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEaSERKSA_(IntrusivePtr* self, const IntrusivePtr* source) { - ReleaseIntrusivePtr(self, 0x30, __func__); - CopyIntrusivePtr(self, source, 0x30); + AssignIntrusivePtr(self, source, 0x30, __func__); } int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEppEv() { @@ -3228,4 +3265,391 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEdeEv() { NotImplemented_nid_no_patch(__func__); return 0; } + +// -------------------------------------------------------------------------- +// UserProfile2V1 model accessors, CommunicationRestrictionStatus plumbing and +// the remaining Common instantiations, from the same dump whose modules lack +// libSceNpCppWebApi.prx. +// -------------------------------------------------------------------------- + +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V112BasicProfile19personalDetailIsSetEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi28ParameterToGetPublicProfiles9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V112BasicProfileC1EPNS1_6Common10LibContextE() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi28ParameterToGetPublicProfiles10initializeEPNS1_6Common10LibContextEPKc() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V112BasicProfile25isOfficiallyVerifiedIsSetEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V113BasicPresence14accountIdIsSetEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V112BasicProfileEEEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V113BasicPresence12setAccountIdERKm() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V113BasicPresenceC1EPNS1_6Common10LibContextE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V114PersonalDetailEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V113BasicPresence12setInContextERKb() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V112BasicProfileEEEEEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends9setoffsetEi() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V112BasicProfile11setOnlineIdERK13SceNpOnlineId() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEE3getEv(const IntrusivePtr* self) { + return self->object; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V112BasicProfile17getPersonalDetailEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1ERKSB_(void* self) { + (void)self; + return 0; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V112BasicProfileEEEEEEptEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V112BasicProfile11getOnlineIdEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEaSERKSA_(IntrusivePtr* self, const IntrusivePtr* source) { + ReleaseIntrusivePtr(self, 0x30, __func__); + CopyIntrusivePtr(self, source, 0x30); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEEE5beginEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V114PersonalDetail14getDisplayNameEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V112BasicProfile13onlineIdIsSetEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V113BasicPresence14inContextIsSetEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv() { + return 0; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V114PersonalDetailEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEC1ERKS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, 0x00); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V112BasicProfileEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V112BasicProfileD1Ev() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEEEneERKS9_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, 0x00); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V112BasicProfile23getIsOfficiallyVerifiedEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V112BasicProfileEEEEEE3getEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi17getPublicProfilesEiRKNS4_28ParameterToGetPublicProfilesERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_25GetPublicProfilesResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V112BasicProfileEEEEEEaSERKSA_(IntrusivePtr* self, const IntrusivePtr* source) { + ReleaseIntrusivePtr(self, 0x30, __func__); + CopyIntrusivePtr(self, source, 0x30); +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv() { + return 0; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, 0x00); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V125GetPublicProfilesResponse11getProfilesEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEEEppEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends8setlimitEi() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V112BasicProfile23setIsOfficiallyVerifiedERKb() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V113BasicPresence17onlineStatusIsSetEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi28ParameterToGetPublicProfilesD1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi28ParameterToGetPublicProfilesC1Ev() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEaSERKS7_(IntrusivePtr* self, const IntrusivePtr* source) { + ReleaseIntrusivePtr(self, 0x00, __func__); + CopyIntrusivePtr(self, source, 0x00); +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V113BasicPresenceD1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEEED2Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers8setlimitEi() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEptEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers9setoffsetEi() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEEE3endEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V113BasicPresence15setOnlineStatusERKNS3_12OnlineStatusE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEEEdeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEEaSERKSB_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + } diff --git a/core/libs/prx/libSceNpCppWebApi/include/Api.hpp b/core/libs/prx/libSceNpCppWebApi/include/Api.hpp index cdb32647a1..77fa374881 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(); @@ -681,4 +681,92 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String5emptyEv(const CommonString* se void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEE3getEv(const IntrusivePtr* self); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEdeEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi28ParameterToGetPublicProfilesD1Ev(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEEEdeEv(); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEED1Ev(IntrusivePtr* self); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEC1ERKS7_(IntrusivePtr* self, const IntrusivePtr* source); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends8setlimitEi(); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEE3getEv(const IntrusivePtr* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEEaSERKSB_(); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V112BasicProfileEED1Ev(IntrusivePtr* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V113BasicPresenceD1Ev(); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEptEv(const IntrusivePtr* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi28ParameterToGetPublicProfiles9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V125GetPublicProfilesResponse11getProfilesEv(); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V112BasicProfileEEEEEEaSERKSA_(IntrusivePtr* self, const IntrusivePtr* source); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEED1Ev(IntrusivePtr* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V112BasicProfile17getPersonalDetailEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi17getPublicProfilesEiRKNS4_28ParameterToGetPublicProfilesERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_25GetPublicProfilesResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V113BasicPresence12setInContextERKb(); +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V112BasicProfile13onlineIdIsSetEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_(); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEaSERKS7_(IntrusivePtr* self, const IntrusivePtr* source); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V113BasicPresence15setOnlineStatusERKNS3_12OnlineStatusE(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V112BasicProfile23setIsOfficiallyVerifiedERKb(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V112BasicProfileC1EPNS1_6Common10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEEED2Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi28ParameterToGetPublicProfiles10initializeEPNS1_6Common10LibContextEPKc(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEC1Ev(IntrusivePtr* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V112BasicProfile23getIsOfficiallyVerifiedEv(); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V114PersonalDetailEEptEv(const IntrusivePtr* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_(); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEC1Ev(IntrusivePtr* self); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEED1Ev(IntrusivePtr* self); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEEE3endEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V113BasicPresence17onlineStatusIsSetEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V113BasicPresenceC1EPNS1_6Common10LibContextE(); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEaSERKSA_(IntrusivePtr* self, const IntrusivePtr* source); +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V112BasicProfile25isOfficiallyVerifiedIsSetEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V113BasicPresence14inContextIsSetEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V112BasicProfileD1Ev(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEEEppEv(); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V112BasicProfileEEEEEE3getEv(const IntrusivePtr* self); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEC1Ev(IntrusivePtr* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V112BasicProfile11setOnlineIdERK13SceNpOnlineId(); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEC1Ev(IntrusivePtr* self); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEED1Ev(IntrusivePtr* self); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V112BasicProfileEEEEEEC1Ev(IntrusivePtr* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V113BasicPresence14accountIdIsSetEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends9setoffsetEi(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V112BasicProfile11getOnlineIdEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1ERKSB_(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V113BasicPresence12setAccountIdERKm(); +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V112BasicProfile19personalDetailIsSetEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEEEneERKS9_(); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V114PersonalDetailEED1Ev(IntrusivePtr* self); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv(); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V112BasicProfileEEEEEEptEv(const IntrusivePtr* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEC1Ev(IntrusivePtr* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEC1Ev(IntrusivePtr* self); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEEE5beginEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V114PersonalDetail14getDisplayNameEv(); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEED1Ev(IntrusivePtr* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi28ParameterToGetPublicProfilesC1Ev(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv(); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEED1Ev(IntrusivePtr* self); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEptEv(const IntrusivePtr* self); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers8setlimitEi(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers9setoffsetEi(); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V112BasicProfileEEEEEED1Ev(IntrusivePtr* self); + } diff --git a/core/libs/prx/libSceNpGameIntent/Export.cpp b/core/libs/prx/libSceNpGameIntent/Export.cpp index 751b4b1c5b..4e6e094e72 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,64 @@ namespace { +constexpr int NpGameIntentErrorNotInitialized = -2141898750; constexpr int NpGameIntentErrorInvalidArgument = -2141898748; constexpr int NpGameIntentErrorIntentNotFound = -2141898746; constexpr int NpGameIntentErrorValueNotFound = -2141898745; constexpr int NpGameIntentUserIdInvalid = -1; +constexpr int NpAppLauncherErrorNotInitialized = -2141898238; +constexpr int NpAppLauncherErrorInvalidArgument = -2141898236; + +std::atomic_bool& NpGameIntentInitialized() { + static std::atomic_bool initialized{false}; + return initialized; +} + +std::atomic_bool& AppLauncherInitialized() { + 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) { @@ -76,15 +98,20 @@ int APS5_VABI sceNpGameIntentVshTerminate(void) { int APS5_VABI sceNpAppLauncherInitialize(const void* initParam) { (void)initParam; + AppLauncherInitialized() = true; return 0; } -int APS5_VABI sceNpAppLauncherLaunchApp() { +int APS5_VABI sceNpAppLauncherLaunchApp(std::int32_t arg0, const char* arg2, const char* arg3) { + (void)arg0; + if (!AppLauncherInitialized()) return NpAppLauncherErrorNotInitialized; + if (arg2 == nullptr || *arg2 == '\0' || (arg3 != nullptr && std::memchr(arg3, '\0', 0x2001) == nullptr)) return NpAppLauncherErrorInvalidArgument; NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceNpAppLauncherTerminate() { + AppLauncherInitialized() = false; return 0; } diff --git a/core/libs/prx/libSceNpGameIntent/include/Api.hpp b/core/libs/prx/libSceNpGameIntent/include/Api.hpp index abd369f817..fbc2ef6e26 100644 --- a/core/libs/prx/libSceNpGameIntent/include/Api.hpp +++ b/core/libs/prx/libSceNpGameIntent/include/Api.hpp @@ -13,7 +13,7 @@ int APS5_VABI sceNpGameIntentInfoInit(NpGameIntentInfo* info); int APS5_VABI sceNpGameIntentVshInitialize(void); int APS5_VABI sceNpGameIntentVshTerminate(void); int APS5_VABI sceNpAppLauncherInitialize(const void* initParam); -int APS5_VABI sceNpAppLauncherLaunchApp(); +int APS5_VABI sceNpAppLauncherLaunchApp(std::int32_t arg0, const char* arg2, const char* arg3); int APS5_VABI sceNpAppLauncherTerminate(); int APS5_VABI sceNpGameIntentGetUdsNpComId(); int APS5_VABI sceNpGameIntentLaunchApp(); diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index 55f1c4c5c8..e357efce1b 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -5,19 +5,28 @@ #include "prx/libc/include/General.hpp" #include "prx/libSceUserService/UserService.hpp" #include +#include #include #include #include // PSN is not emulated: the user is reported as signed out and online queries fail. static constexpr int SCE_NP_ERROR_INVALID_ARGUMENT = static_cast(0x80550003); +static constexpr int SCE_NP_ERROR_INVALID_PLATFORM_TYPE = static_cast(0x80550004); +static constexpr int SCE_NP_ERROR_OUT_OF_MEMORY = static_cast(0x80550005); static constexpr int SCE_NP_ERROR_SIGNED_OUT = static_cast(0x80550006); static constexpr int SCE_NP_ERROR_USER_NOT_FOUND = static_cast(0x80550007); static constexpr int SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED = static_cast(0x80550008); static constexpr int SCE_NP_ERROR_CALLBACK_NOT_REGISTERED = static_cast(0x80550009); static constexpr int SCE_NP_ERROR_INVALID_SIZE = static_cast(0x80550011); static constexpr int SCE_NP_ERROR_REQUEST_NOT_FOUND = static_cast(0x80550014); +static constexpr int SCE_NP_ERROR_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 +48,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 +301,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 +315,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 +388,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__); @@ -381,13 +408,18 @@ int APS5_VABI sceNpManagerIntAddSigninStateCallback(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpManagerIntAuthGetAuthorizationCode(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpManagerIntAuthGetAuthorizationCode(int reqId, int userId, const void* parameter, const void* optionalParameter, std::uint32_t firstValue, std::uint32_t secondValue, std::uint8_t flag, void* output) { + (void)reqId; + (void)optionalParameter; + (void)flag; + if (userId == USER_SERVICE_USER_ID_INVALID || !parameter || !output || firstValue > 3 || secondValue < 1 || secondValue > 3) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpManagerIntAuthGetIdToken(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpManagerIntAuthGetIdToken(int reqId, int userId, const void* firstParameter, const void* secondParameter, const void* thirdParameter, std::uint64_t value, void* output) { + (void)reqId; + (void)value; + if (userId == USER_SERVICE_USER_ID_INVALID || !firstParameter || !secondParameter || !thirdParameter || !output) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpManagerIntCheckNpAvailability(void) { NotImplemented_nid_no_patch(__func__); @@ -438,32 +470,37 @@ 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__); return 0; } -int APS5_VABI sceNpManagerIntGetAccountAge(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpManagerIntGetAccountAge(int reqId, int userId, std::uint8_t* age) { + (void)reqId; + if (userId == USER_SERVICE_USER_ID_INVALID || !age) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpManagerIntGetAccountCountry(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpManagerIntGetAccountCountryA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpManagerIntGetAccountCountryA(int userId, void* countryCode) { + if (userId == USER_SERVICE_USER_ID_INVALID || !countryCode) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpManagerIntGetAccountCountrySdk(void) { NotImplemented_nid_no_patch(__func__); @@ -485,9 +522,10 @@ int APS5_VABI sceNpManagerIntGetAccountLanguage(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpManagerIntGetAccountLanguageA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpManagerIntGetAccountLanguageA(int reqId, int userId, void* language) { + (void)reqId; + if (userId == USER_SERVICE_USER_ID_INVALID || !language) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpManagerIntGetAccountNpEnv(void) { NotImplemented_nid_no_patch(__func__); @@ -505,13 +543,22 @@ int APS5_VABI sceNpManagerIntGetAuthServerErrorFlag(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpManagerIntGetAuthorizationCode(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpManagerIntGetAuthorizationCode(int reqId, int userId, const void* firstParameter, const void* secondParameter, const void* optionalParameter, std::uint32_t mode, std::uint8_t flag, void* optionalOutput, void* output) { + (void)reqId; + (void)optionalParameter; + (void)flag; + (void)optionalOutput; + if (userId == USER_SERVICE_USER_ID_INVALID || !firstParameter || !secondParameter || !output) return SCE_NP_ERROR_INVALID_ARGUMENT; + if (mode != 0 && mode != 3 && mode != 5) return SCE_NP_ERROR_INVALID_PLATFORM_TYPE; + return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpManagerIntGetAuthorizationCodeWithPsnoUri(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpManagerIntGetAuthorizationCodeWithPsnoUri(int reqId, int userId, const char* uri, std::uint32_t mode, void* output, std::size_t size) { + (void)reqId; + if (userId == USER_SERVICE_USER_ID_INVALID || !uri || !output) return SCE_NP_ERROR_INVALID_ARGUMENT; + if (size < 1 || size > 0x801) return SCE_NP_ERROR_INVALID_SIZE; + if (mode != 0 && mode != 3 && mode != 5) return SCE_NP_ERROR_INVALID_PLATFORM_TYPE; + if (!std::memchr(uri, 0, 0x2000)) return SCE_NP_ERROR_OUT_OF_MEMORY; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpManagerIntGetClientCredentialAccessToken(void) { NotImplemented_nid_no_patch(__func__); @@ -533,9 +580,9 @@ int APS5_VABI sceNpManagerIntGetNpEnv(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpManagerIntGetNpId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpManagerIntGetNpId(int userId, NpId* npId) { + if (userId == USER_SERVICE_USER_ID_INVALID || !npId) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpManagerIntGetNpIdByOnlineId(void) { NotImplemented_nid_no_patch(__func__); @@ -549,9 +596,9 @@ int APS5_VABI sceNpManagerIntGetOfflineAccountId(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpManagerIntGetOnlineId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpManagerIntGetOnlineId(int userId, NpOnlineId* onlineId) { + if (userId == USER_SERVICE_USER_ID_INVALID || !onlineId) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpManagerIntGetOnlineIdChangeFlag(void) { NotImplemented_nid_no_patch(__func__); @@ -609,13 +656,13 @@ int APS5_VABI sceNpManagerIntGetUserIdByAccountId(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpManagerIntGetUserIdByNpId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpManagerIntGetUserIdByNpId(const NpId* npId, int* userId) { + if (!npId || !userId) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpManagerIntGetUserIdByOnlineId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpManagerIntGetUserIdByOnlineId(const NpOnlineId* onlineId, int* userId) { + if (!onlineId || !userId) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpManagerIntGetUserIdByOnlineIdSdk(void) { NotImplemented_nid_no_patch(__func__); @@ -955,7 +1002,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..e862a72c1f 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,13 +81,13 @@ 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); int APS5_VABI sceNpManagerIntAddSigninStateCallback(void); -int APS5_VABI sceNpManagerIntAuthGetAuthorizationCode(void); -int APS5_VABI sceNpManagerIntAuthGetIdToken(void); +int APS5_VABI sceNpManagerIntAuthGetAuthorizationCode(int reqId, int userId, const void* parameter, const void* optionalParameter, std::uint32_t firstValue, std::uint32_t secondValue, std::uint8_t flag, void* output); +int APS5_VABI sceNpManagerIntAuthGetIdToken(int reqId, int userId, const void* firstParameter, const void* secondParameter, const void* thirdParameter, std::uint64_t value, void* output); int APS5_VABI sceNpManagerIntCheckNpAvailability(void); int APS5_VABI sceNpManagerIntCheckNpAvailabilityByPid(void); int APS5_VABI sceNpManagerIntCheckNpState(void); @@ -102,33 +102,33 @@ 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 sceNpManagerIntGetAccountAge(int reqId, int userId, std::uint8_t* age); int APS5_VABI sceNpManagerIntGetAccountCountry(void); -int APS5_VABI sceNpManagerIntGetAccountCountryA(void); +int APS5_VABI sceNpManagerIntGetAccountCountryA(int userId, void* countryCode); int APS5_VABI sceNpManagerIntGetAccountCountrySdk(void); int APS5_VABI sceNpManagerIntGetAccountDateOfBirthSdk(void); int APS5_VABI sceNpManagerIntGetAccountId(void); int APS5_VABI sceNpManagerIntGetAccountIdSdk(void); int APS5_VABI sceNpManagerIntGetAccountLanguage(void); -int APS5_VABI sceNpManagerIntGetAccountLanguageA(void); +int APS5_VABI sceNpManagerIntGetAccountLanguageA(int reqId, int userId, void* language); int APS5_VABI sceNpManagerIntGetAccountNpEnv(void); int APS5_VABI sceNpManagerIntGetAccountType(void); int APS5_VABI sceNpManagerIntGetActiveSigninState(void); int APS5_VABI sceNpManagerIntGetAuthServerErrorFlag(void); -int APS5_VABI sceNpManagerIntGetAuthorizationCode(void); -int APS5_VABI sceNpManagerIntGetAuthorizationCodeWithPsnoUri(void); +int APS5_VABI sceNpManagerIntGetAuthorizationCode(int reqId, int userId, const void* firstParameter, const void* secondParameter, const void* optionalParameter, std::uint32_t mode, std::uint8_t flag, void* optionalOutput, void* output); +int APS5_VABI sceNpManagerIntGetAuthorizationCodeWithPsnoUri(int reqId, int userId, const char* uri, std::uint32_t mode, void* output, std::size_t size); int APS5_VABI sceNpManagerIntGetClientCredentialAccessToken(void); int APS5_VABI sceNpManagerIntGetCommunicationRestrictionStatus(void); int APS5_VABI sceNpManagerIntGetGameAccessToken(void); int APS5_VABI sceNpManagerIntGetIssuerId(void); int APS5_VABI sceNpManagerIntGetNpEnv(void); -int APS5_VABI sceNpManagerIntGetNpId(void); +int APS5_VABI sceNpManagerIntGetNpId(int userId, NpId* npId); int APS5_VABI sceNpManagerIntGetNpIdByOnlineId(void); int APS5_VABI sceNpManagerIntGetNpIdSdk(void); int APS5_VABI sceNpManagerIntGetOfflineAccountId(void); -int APS5_VABI sceNpManagerIntGetOnlineId(void); +int APS5_VABI sceNpManagerIntGetOnlineId(int userId, NpOnlineId* onlineId); int APS5_VABI sceNpManagerIntGetOnlineIdChangeFlag(void); int APS5_VABI sceNpManagerIntGetOnlineIdSdk(void); int APS5_VABI sceNpManagerIntGetParentalControlFlag(void); @@ -143,8 +143,8 @@ int APS5_VABI sceNpManagerIntGetTicketA(void); int APS5_VABI sceNpManagerIntGetTitleToken(void); int APS5_VABI sceNpManagerIntGetTitleTokenWithCheck(void); int APS5_VABI sceNpManagerIntGetUserIdByAccountId(void); -int APS5_VABI sceNpManagerIntGetUserIdByNpId(void); -int APS5_VABI sceNpManagerIntGetUserIdByOnlineId(void); +int APS5_VABI sceNpManagerIntGetUserIdByNpId(const NpId* npId, int* userId); +int APS5_VABI sceNpManagerIntGetUserIdByOnlineId(const NpOnlineId* onlineId, int* userId); int APS5_VABI sceNpManagerIntGetUserIdByOnlineIdSdk(void); int APS5_VABI sceNpManagerIntGetUserList(void); int APS5_VABI sceNpManagerIntGetUserNum(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/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/CMakeLists.txt b/core/libs/prx/libSceNpTrophy2/CMakeLists.txt index a7189fbc13..7f9d3efe3a 100644 --- a/core/libs/prx/libSceNpTrophy2/CMakeLists.txt +++ b/core/libs/prx/libSceNpTrophy2/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(libSceNpTrophy2 SHARED EXCLUDE_FROM_ALL + ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Context.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Handle.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/GameInfo.cpp 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/CMakeLists.txt b/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt index 62016d6a55..9d02b23500 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt +++ b/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(libSceNpUniversalDataSystem SHARED EXCLUDE_FROM_ALL + ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/PendingExports.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/System.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Context.cpp 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/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/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 59d7ed5e61..9f28aee996 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) @@ -56,3 +66,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_raw_resolver_tests ${LIBS_INCLUDE_DIR}/tests/ResolverRaw.cpp) + target_include_directories(guest_raw_resolver_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_raw_resolver_tests PRIVATE libScePosixForWebKit libc) + configure_windows_unwind(guest_raw_resolver_tests) + add_test(NAME guest_raw_resolver COMMAND guest_raw_resolver_tests) + set_tests_properties(guest_raw_resolver PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_raw_resolver 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 9f7f7de9a5..14675b52d3 100644 --- a/core/libs/prx/libScePosixForWebKit/Export.cpp +++ b/core/libs/prx/libScePosixForWebKit/Export.cpp @@ -201,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); @@ -214,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); @@ -227,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__); diff --git a/core/libs/prx/libScePosixForWebKit/Resolver.cpp b/core/libs/prx/libScePosixForWebKit/Resolver.cpp index 6b0007a4b9..f8e439dc79 100644 --- a/core/libs/prx/libScePosixForWebKit/Resolver.cpp +++ b/core/libs/prx/libScePosixForWebKit/Resolver.cpp @@ -260,15 +260,17 @@ int APS5_VABI getaddrinfo_nid_postfix(const char* node, const char* service, const GuestResolver::AddressInfo* hints, GuestResolver::AddressInfo** output) { if (!output) return 4; *output = nullptr; + if (!node && !service) return 8; addrinfo nativeHints{}; + const bool rawQuery = hints && hints->socketType == 3; if (hints) { if (hints->addressLength || hints->address || hints->canonicalName || hints->next) return 12; if ((hints->flags & ~0xd0f) != 0) return 3; if (hints->family != 0 && hints->family != 2 && hints->family != 28) return 5; - if (hints->socketType != 0 && hints->socketType != 1 && hints->socketType != 2) return 10; + if (hints->socketType != 0 && hints->socketType != 1 && hints->socketType != 2 && hints->socketType != 3) return 10; nativeHints.ai_family = hints->family == 28 ? AF_INET6 : hints->family == 2 ? AF_INET : AF_UNSPEC; - nativeHints.ai_socktype = hints->socketType == 1 ? SOCK_STREAM : hints->socketType == 2 ? SOCK_DGRAM : 0; - nativeHints.ai_protocol = hints->protocol; + nativeHints.ai_socktype = hints->socketType == 1 ? SOCK_STREAM : (hints->socketType == 2 || rawQuery) ? SOCK_DGRAM : 0; + nativeHints.ai_protocol = rawQuery ? IPPROTO_UDP : hints->protocol; if (hints->flags & 1) nativeHints.ai_flags |= AI_PASSIVE; if (hints->flags & 2) nativeHints.ai_flags |= AI_CANONNAME; if (hints->flags & 4) nativeHints.ai_flags |= AI_NUMERICHOST; @@ -277,6 +279,7 @@ int APS5_VABI getaddrinfo_nid_postfix(const char* node, const char* service, if (hints->flags & 0x400) nativeHints.ai_flags |= AI_ADDRCONFIG; if (hints->flags & 0x800) nativeHints.ai_flags |= AI_V4MAPPED; } + if (rawQuery && service) return 9; if (!Ready()) return 4; addrinfo* native = nullptr; const auto error = ::getaddrinfo(node, service, &nativeHints, &native); @@ -292,8 +295,8 @@ int APS5_VABI getaddrinfo_nid_postfix(const char* node, const char* service, tail = &converted->next; converted->flags = hints ? hints->flags : 0; converted->family = item->ai_family == AF_INET ? 2 : 28; - converted->socketType = item->ai_socktype == SOCK_STREAM ? 1 : item->ai_socktype == SOCK_DGRAM ? 2 : 0; - converted->protocol = item->ai_protocol; + converted->socketType = rawQuery ? 3 : item->ai_socktype == SOCK_STREAM ? 1 : item->ai_socktype == SOCK_DGRAM ? 2 : 0; + converted->protocol = rawQuery ? hints->protocol : item->ai_protocol; converted->addressLength = item->ai_family == AF_INET ? 16 : 28; auto* bytes = static_cast(std::calloc(1, converted->addressLength)); converted->address = bytes; 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 bff7385004..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,15 +12,15 @@ 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(); diff --git a/core/libs/prx/libScePsml_debug/Export.cpp b/core/libs/prx/libScePsml_debug/Export.cpp index 02677de85c..c93e6d9ca8 100644 --- a/core/libs/prx/libScePsml_debug/Export.cpp +++ b/core/libs/prx/libScePsml_debug/Export.cpp @@ -18,6 +18,9 @@ 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; @@ -151,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; } 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/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 91c6591395..b53df446cf 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -534,90 +534,6 @@ int APS5_VABI sceRtcSetDosTime(RtcDateTime* time, uint32_t dos_time) { return 0; } -int APS5_VABI sceRtcGetYear(const RtcDateTime* time) { - if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - return static_cast(time->year); -} - -int APS5_VABI sceRtcSetYear(RtcDateTime* time, int year) { - if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - if (year < 1 || year > 9999) return SCE_RTC_ERROR_INVALID_YEAR; - time->year = static_cast(year); - return 0; -} - -int APS5_VABI sceRtcGetMonth(const RtcDateTime* time) { - if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - return static_cast(time->month); -} - -int APS5_VABI sceRtcSetMonth(RtcDateTime* time, int month) { - if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - if (month < 1 || month > 12) return SCE_RTC_ERROR_INVALID_MONTH; - time->month = static_cast(month); - return 0; -} - -int APS5_VABI sceRtcGetDay(const RtcDateTime* time) { - if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - return static_cast(time->day); -} - -int APS5_VABI sceRtcSetDay(RtcDateTime* time, int day) { - if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - if (day < 1 || day > 31) return SCE_RTC_ERROR_INVALID_DAY; - time->day = static_cast(day); - return 0; -} - -int APS5_VABI sceRtcGetHour(const RtcDateTime* time) { - if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - return static_cast(time->hour); -} - -int APS5_VABI sceRtcSetHour(RtcDateTime* time, int hour) { - if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - if (hour < 0 || hour > 23) return SCE_RTC_ERROR_INVALID_HOUR; - time->hour = static_cast(hour); - return 0; -} - -int APS5_VABI sceRtcGetMinute(const RtcDateTime* time) { - if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - return static_cast(time->minute); -} - -int APS5_VABI sceRtcSetMinute(RtcDateTime* time, int minute) { - if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - if (minute < 0 || minute > 59) return SCE_RTC_ERROR_INVALID_MINUTE; - time->minute = static_cast(minute); - return 0; -} - -int APS5_VABI sceRtcGetSecond(const RtcDateTime* time) { - if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - return static_cast(time->second); -} - -int APS5_VABI sceRtcSetSecond(RtcDateTime* time, int second) { - if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - if (second < 0 || second > 59) return SCE_RTC_ERROR_INVALID_SECOND; - time->second = static_cast(second); - return 0; -} - -int APS5_VABI sceRtcGetMicrosecond(const RtcDateTime* time) { - if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - return static_cast(time->microsecond); -} - -int APS5_VABI sceRtcSetMicrosecond(RtcDateTime* time, int microsecond) { - if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - if (microsecond < 0 || static_cast(microsecond) >= TICKS_PER_SECOND) return SCE_RTC_ERROR_INVALID_MICROSECOND; - time->microsecond = static_cast(microsecond); - return 0; -} - int APS5_VABI sceRtcFormatRFC3339(char* dateTime, const RtcTick* utc, int timeZoneMinutes) { if (!dateTime) return SCE_RTC_ERROR_INVALID_POINTER; RtcTick now{}; diff --git a/core/libs/prx/libSceRtc/include/Rtc.hpp b/core/libs/prx/libSceRtc/include/Rtc.hpp index 7e84acf6b7..2e570dc02b 100644 --- a/core/libs/prx/libSceRtc/include/Rtc.hpp +++ b/core/libs/prx/libSceRtc/include/Rtc.hpp @@ -23,20 +23,6 @@ int APS5_VABI sceRtcGetWin32FileTime(const RtcDateTime* time, uint64_t* win32Tim int APS5_VABI sceRtcSetWin32FileTime(RtcDateTime* time, uint64_t win32Time); int APS5_VABI sceRtcGetDosTime(const RtcDateTime* time, uint32_t* dosTime); int APS5_VABI sceRtcSetDosTime(RtcDateTime* time, uint32_t dosTime); -int APS5_VABI sceRtcGetYear(const RtcDateTime* time); -int APS5_VABI sceRtcSetYear(RtcDateTime* time, int year); -int APS5_VABI sceRtcGetMonth(const RtcDateTime* time); -int APS5_VABI sceRtcSetMonth(RtcDateTime* time, int month); -int APS5_VABI sceRtcGetDay(const RtcDateTime* time); -int APS5_VABI sceRtcSetDay(RtcDateTime* time, int day); -int APS5_VABI sceRtcGetHour(const RtcDateTime* time); -int APS5_VABI sceRtcSetHour(RtcDateTime* time, int hour); -int APS5_VABI sceRtcGetMinute(const RtcDateTime* time); -int APS5_VABI sceRtcSetMinute(RtcDateTime* time, int minute); -int APS5_VABI sceRtcGetSecond(const RtcDateTime* time); -int APS5_VABI sceRtcSetSecond(RtcDateTime* time, int second); -int APS5_VABI sceRtcGetMicrosecond(const RtcDateTime* time); -int APS5_VABI sceRtcSetMicrosecond(RtcDateTime* time, int microsecond); int APS5_VABI sceRtcFormatRFC3339(char* dateTime, const RtcTick* utc, int timeZoneMinutes); int APS5_VABI sceRtcFormatRFC3339LocalTime(char* dateTime, const RtcTick* utc); int APS5_VABI sceRtcFormatRFC2822(char* dateTime, const RtcTick* utc, int timeZoneMinutes); diff --git a/core/libs/prx/libSceRudp/CMakeLists.txt b/core/libs/prx/libSceRudp/CMakeLists.txt index 962bd86e42..2f8f0c7420 100644 --- a/core/libs/prx/libSceRudp/CMakeLists.txt +++ b/core/libs/prx/libSceRudp/CMakeLists.txt @@ -4,3 +4,16 @@ add_library(libSceRudp SHARED EXCLUDE_FROM_ALL ) target_include_directories(libSceRudp PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceRudp) + +if(BUILD_TESTING) + add_test_executable(guest_rudp_empty_poll_tests ${LIBS_INCLUDE_DIR}/tests/GuestRudpEmptyPoll.cpp) + target_include_directories(guest_rudp_empty_poll_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_rudp_empty_poll_tests PRIVATE libSceRudp libkernel libc) + configure_windows_unwind(guest_rudp_empty_poll_tests) + add_test(NAME guest_rudp_empty_poll COMMAND guest_rudp_empty_poll_tests) + set_tests_properties(guest_rudp_empty_poll PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_rudp_empty_poll PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceSaveData.native/CMakeLists.txt b/core/libs/prx/libSceSaveData.native/CMakeLists.txt index 46d6b8bba9..5236fba466 100644 --- a/core/libs/prx/libSceSaveData.native/CMakeLists.txt +++ b/core/libs/prx/libSceSaveData.native/CMakeLists.txt @@ -26,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/libSceSpSysCallWrapper/Export.cpp b/core/libs/prx/libSceSpSysCallWrapper/Export.cpp index f0db9348f5..12371b71f2 100644 --- a/core/libs/prx/libSceSpSysCallWrapper/Export.cpp +++ b/core/libs/prx/libSceSpSysCallWrapper/Export.cpp @@ -183,9 +183,8 @@ 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(PthreadMutexattr* attr, int protocol) { diff --git a/core/libs/prx/libSceSpSysCallWrapper/include/Api.hpp b/core/libs/prx/libSceSpSysCallWrapper/include/Api.hpp index 10979d9d31..2ef4e11459 100644 --- a/core/libs/prx/libSceSpSysCallWrapper/include/Api.hpp +++ b/core/libs/prx/libSceSpSysCallWrapper/include/Api.hpp @@ -49,7 +49,7 @@ 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(); +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); diff --git a/core/libs/prx/libSceSsl/CMakeLists.txt b/core/libs/prx/libSceSsl/CMakeLists.txt index 8296a60735..cf5b24b014 100644 --- a/core/libs/prx/libSceSsl/CMakeLists.txt +++ b/core/libs/prx/libSceSsl/CMakeLists.txt @@ -60,6 +60,8 @@ target_link_libraries(libSceSsl PRIVATE mbedtls mbedx509 mbedcrypto) set_target_properties(libSceSsl PROPERTIES VISIBILITY_INLINES_HIDDEN ON) if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(libSceSsl PRIVATE LINKER:--exclude-libs,ALL) +elseif(APPLE) + set_target_properties(mbedtls mbedcrypto mbedx509 p256m PROPERTIES C_VISIBILITY_PRESET hidden) endif() if(WIN32) target_link_libraries(libSceSsl PRIVATE bcrypt ws2_32) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index a3ae4afcec..c8a56dc09d 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -998,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"); 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/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 14b0d27a7e..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:$") 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/DisplayWindow.cpp b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp index 35c6c86917..ad6f310991 100644 --- a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp +++ b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp @@ -7,6 +7,8 @@ #include "SDL_vulkan.h" #include #include +#include +#include #include #include #include @@ -61,7 +63,9 @@ void DisplayWindow::create(std::uint32_t sourceWidth, std::uint32_t sourceHeight require(initialSize.width >= DisplayWindowMinimumWidth && initialSize.height >= DisplayWindowMinimumHeight, "initial window extent is smaller than the minimum"); const auto title = GetAppTitle_nid_postfix(); AgcDriverLockVulkanLoader_nid_postfix(); - window = SDL_CreateWindow(title.value, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, static_cast(initialSize.width), static_cast(initialSize.height), SDL_WINDOW_SHOWN | SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); + const char* fullscreen = std::getenv("ANYPS5_FULLSCREEN"); + const Uint32 fullscreenFlag = fullscreen != nullptr && *fullscreen != '\0' && std::strcmp(fullscreen, "0") != 0 ? static_cast(SDL_WINDOW_FULLSCREEN_DESKTOP) : 0u; + window = SDL_CreateWindow(title.value, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, static_cast(initialSize.width), static_cast(initialSize.height), SDL_WINDOW_SHOWN | SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | fullscreenFlag); AgcDriverUnlockVulkanLoader_nid_postfix(); require(window != nullptr, SDL_GetError()); SDL_SetWindowMinimumSize(window, static_cast(DisplayWindowMinimumWidth), static_cast(DisplayWindowMinimumHeight)); 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/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index de23de7b72..df09695444 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -38,6 +38,7 @@ constexpr int Videodec2ErrorAccessUnitPointer = static_cast(0x811D010Eu); static_assert(sizeof(Vp9PictureInfo) == 88); static_assert(offsetof(Vp9PictureInfo, colorSpace) == 60); +static_assert(sizeof(OutputInfo) == 0x38 && offsetof(OutputInfo, isDiscardedFrame) == 0xb && offsetof(OutputInfo, codecType) == 0xc && offsetof(OutputInfo, frameBuffer) == 0x20); class Vp9Bits { public: @@ -458,6 +459,7 @@ void EmitDecoded(std::uint64_t handle, Decoder& decoder, FrameBuffer* frame, Out output->isValid = true; output->isErrorFrame = false; output->pictureCount = 1; + output->isDiscardedFrame = false; output->frameWidth = decoder.width; output->framePitch = pitch / decoder.sampleBytes; output->frameHeight = decoder.height; diff --git a/core/libs/prx/libSceVideodec2/include/Api.hpp b/core/libs/prx/libSceVideodec2/include/Api.hpp index ff71178c8c..b338ef37c4 100644 --- a/core/libs/prx/libSceVideodec2/include/Api.hpp +++ b/core/libs/prx/libSceVideodec2/include/Api.hpp @@ -101,6 +101,7 @@ struct OutputInfo { bool isValid; bool isErrorFrame; std::uint8_t pictureCount; + bool isDiscardedFrame; std::uint32_t codecType; std::uint32_t frameWidth; std::uint32_t framePitch; 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..cb5daf8bbf 100644 --- a/core/libs/prx/libSceVoiceChat/Export.cpp +++ b/core/libs/prx/libSceVoiceChat/Export.cpp @@ -25,14 +25,19 @@ void logSoft(const char* func, const char* what) { std::fprintf(stderr, "[SOFT-VOICECHAT] %s -> %s\n", func, what); std::fflush(stderr); } + +int offlineRequest(const char* func) { + logSoft(func, "SIGNED_OUT (offline)"); + return VOICECHAT_ERROR_SIGNED_OUT; +} } // namespace 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; } @@ -43,37 +48,28 @@ int APS5_VABI sceVoiceChatCreateRequest(void* a, void* b) { return VOICECHAT_ERROR_SIGNED_OUT; } -#define VOICECHAT_OFFLINE_REQUEST(NAME) \ - int APS5_VABI NAME(int requestId, void* a, void* b) { \ - (void)requestId; \ - (void)a; \ - (void)b; \ - logSoft(#NAME, "SIGNED_OUT (offline)"); \ - return VOICECHAT_ERROR_SIGNED_OUT; \ - } +int APS5_VABI sceVoiceChatRequestRegisterSession(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } +int APS5_VABI sceVoiceChatRequestUnregisterSession(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } +int APS5_VABI sceVoiceChatRequestCreateGameSessionVoiceChatChannel(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } +int APS5_VABI sceVoiceChatRequestDeleteGameSessionVoiceChatChannel(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } +int APS5_VABI sceVoiceChatRequestJoinGameSessionVoiceChatChannel(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } +int APS5_VABI sceVoiceChatRequestLeaveGameSessionVoiceChatChannel(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } +int APS5_VABI sceVoiceChatRequestCreatePlayerSessionVoiceChatChannel(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } +int APS5_VABI sceVoiceChatRequestDeletePlayerSessionVoiceChatChannel(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } +int APS5_VABI sceVoiceChatRequestJoinPlayerSessionVoiceChatChannel(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } +int APS5_VABI sceVoiceChatRequestLeavePlayerSessionVoiceChatChannel(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } +int APS5_VABI sceVoiceChatRequestCreateVoiceChatGroup(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } +int APS5_VABI sceVoiceChatRequestJoinVoiceChatGroup(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } +int APS5_VABI sceVoiceChatRequestLeaveVoiceChatGroup(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } +int APS5_VABI sceVoiceChatRequestDeleteVoiceChatGroup(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestRegisterSession) -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestUnregisterSession) -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestCreateGameSessionVoiceChatChannel) -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestDeleteGameSessionVoiceChatChannel) -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestJoinGameSessionVoiceChatChannel) -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestLeaveGameSessionVoiceChatChannel) -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestCreatePlayerSessionVoiceChatChannel) -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestDeletePlayerSessionVoiceChatChannel) -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestJoinPlayerSessionVoiceChatChannel) -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestLeavePlayerSessionVoiceChatChannel) -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestCreateVoiceChatGroup) -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestJoinVoiceChatGroup) -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestLeaveVoiceChatGroup) -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestDeleteVoiceChatGroup) - -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequest0) -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequest1) -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestAcsTts) -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestAction) -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestSetMemberVolume) -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestUpdateGameSessionVoiceChatChannelName) -VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestUpdatePlayerSessionVoiceChatChannelName) +int APS5_VABI sceVoiceChatRequest0(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } +int APS5_VABI sceVoiceChatRequest1(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } +int APS5_VABI sceVoiceChatRequestAcsTts(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } +int APS5_VABI sceVoiceChatRequestAction(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } +int APS5_VABI sceVoiceChatRequestSetMemberVolume(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } +int APS5_VABI sceVoiceChatRequestUpdateGameSessionVoiceChatChannelName(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } +int APS5_VABI sceVoiceChatRequestUpdatePlayerSessionVoiceChatChannelName(int requestId, void* a, void* b) { (void)requestId; (void)a; (void)b; return offlineRequest(__func__); } int APS5_VABI sceVoiceChatAbortRequest(int requestId) { (void)requestId; diff --git a/core/libs/prx/libSceVoiceChat/include/Api.hpp b/core/libs/prx/libSceVoiceChat/include/Api.hpp index 5d5676dff3..0cb6e115a5 100644 --- a/core/libs/prx/libSceVoiceChat/include/Api.hpp +++ b/core/libs/prx/libSceVoiceChat/include/Api.hpp @@ -6,12 +6,32 @@ 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); -int APS5_VABI NAME(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequestRegisterSession(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequestUnregisterSession(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequestCreateGameSessionVoiceChatChannel(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequestDeleteGameSessionVoiceChatChannel(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequestJoinGameSessionVoiceChatChannel(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequestLeaveGameSessionVoiceChatChannel(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequestCreatePlayerSessionVoiceChatChannel(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequestDeletePlayerSessionVoiceChatChannel(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequestJoinPlayerSessionVoiceChatChannel(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequestLeavePlayerSessionVoiceChatChannel(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequestCreateVoiceChatGroup(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequestJoinVoiceChatGroup(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequestLeaveVoiceChatGroup(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequestDeleteVoiceChatGroup(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequest0(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequest1(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequestAcsTts(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequestAction(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequestSetMemberVolume(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequestUpdateGameSessionVoiceChatChannelName(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatRequestUpdatePlayerSessionVoiceChatChannelName(int requestId, void* a, void* b); int APS5_VABI sceVoiceChatAbortRequest(int requestId); int APS5_VABI sceVoiceChatGetActiveChannel(void* channel); int APS5_VABI sceVoiceChatGetChannelMemberActiveChannelMatchingState(void* a, void* b); diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index bf8e7ab043..670c6d3531 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -23,6 +23,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_stream_purge_tests ${LIBS_INCLUDE_DIR}/tests/GuestStreamPurge.cpp) + target_include_directories(guest_stream_purge_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_stream_purge_tests PRIVATE libc) + configure_windows_unwind(guest_stream_purge_tests) + add_test(NAME guest_stream_purge COMMAND guest_stream_purge_tests) + set_tests_properties(guest_stream_purge PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_stream_purge PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_safe_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestSafeErrors.cpp) target_include_directories(guest_safe_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_safe_error_tests PRIVATE libc) @@ -71,6 +81,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/FileLock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StdioExtras.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StandardStreams.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/StreamPurge.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/GuestAllocations.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/GuestArena.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/GuestStacks.cpp @@ -140,6 +151,21 @@ endif() target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/FloatingPoint.cpp) if(BUILD_TESTING) + add_test_executable(guest_raise_except_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseExcept.cpp) + target_include_directories(guest_raise_except_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_raise_except_tests PRIVATE libc ${CMAKE_DL_LIBS}) + configure_windows_unwind(guest_raise_except_tests) + add_test(NAME guest_raise_except COMMAND guest_raise_except_tests $) + set(raiseExceptTests guest_raise_except) + foreach(exceptionBit RANGE 0 5) + add_test(NAME guest_raise_except_trap_${exceptionBit} COMMAND guest_raise_except_tests $ ${exceptionBit}) + list(APPEND raiseExceptTests guest_raise_except_trap_${exceptionBit}) + endforeach() + set_tests_properties(${raiseExceptTests} PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(${raiseExceptTests} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_floating_point_tests ${LIBS_INCLUDE_DIR}/tests/GuestFloatingPoint.cpp) target_include_directories(guest_floating_point_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_floating_point_tests PRIVATE libc ${CMAKE_DL_LIBS}) @@ -172,12 +198,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) @@ -904,6 +932,7 @@ if(BUILD_TESTING) target_include_directories(unwind_callback_abi_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(unwind_callback_abi_tests PRIVATE libc) configure_windows_unwind(unwind_callback_abi_tests) + configure_macos_unwind(unwind_callback_abi_tests) foreach(callback IN ITEMS backtrace forced_unwind delete_exception end_catch end_catch_gnu) add_test(NAME unwind_callback_abi_${callback} COMMAND unwind_callback_abi_tests ${callback}) if(WIN32) @@ -924,11 +953,30 @@ if(BUILD_TESTING) target_include_directories(guest_bad_optional_access_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_bad_optional_access_tests PRIVATE libc) configure_windows_unwind(guest_bad_optional_access_tests) + configure_macos_unwind(guest_bad_optional_access_tests) add_test(NAME guest_bad_optional_access COMMAND guest_bad_optional_access_tests) if(WIN32) 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 b1e66a54b6..4ac85fedc2 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); @@ -219,12 +228,14 @@ 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); FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const char* filename, int mode, int protection); size_t APS5_VABI fread_nid_postfix(void* buffer, size_t size, size_t count, FileStream* stream); size_t APS5_VABI fwrite_nid_postfix(const void* buffer, size_t size, size_t count, FileStream* stream); +int APS5_VABI fpurge_nid_postfix(FileStream*); int APS5_VABI fseeko_nid_postfix(FileStream* stream, std::int64_t offset, int origin); std::int64_t APS5_VABI ftello_nid_postfix(FileStream* stream); int APS5_VABI fseek_nid_postfix(FileStream* stream, std::int64_t offset, int origin); @@ -324,6 +335,7 @@ int APS5_VABI fegetexceptflag_nid_postfix(std::uint16_t* flags, int exceptions); void APS5_VABI _Feraise_nid_postfix(int exceptions); int APS5_VABI fesetexceptflag_nid_postfix(const std::uint16_t* flags, int exceptions); int APS5_VABI feclearexcept_nid_postfix(int exceptions); +int APS5_VABI feraiseexcept_nid_postfix(int exceptions); int APS5_VABI feenableexcept_nid_postfix(int exceptions); int APS5_VABI fedisableexcept_nid_postfix(int exceptions); int APS5_VABI __feenableexcept_nid_postfix(int exceptions); @@ -355,6 +367,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, ...); @@ -364,6 +383,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); @@ -455,6 +478,8 @@ int APS5_VABI tolower_nid_postfix(int c); void APS5_VABI _ZNSt8ios_baseD2Ev_nid_postfix(GuestLocale::IosBase* self); GuestLocale::IosBase* APS5_VABI SetStreamWidth(GuestLocale::IosBase* stream, std::int32_t width); StreamManipulator APS5_VABI _ZSt4setwi_nid_postfix(std::int32_t width); +GuestLocale::IosBase* APS5_VABI SetStreamPrecision(GuestLocale::IosBase* stream, std::int32_t precision); +StreamManipulator APS5_VABI _ZSt12setprecisioni_nid_postfix(std::int32_t precision); void APS5_VABI _ZNSt8ios_base7_AddstdEPS__nid_postfix(GuestLocale::IosBase* self); GuestLocale::Implementation* APS5_VABI _ZNSt6locale5_InitEv_nid_postfix(); void APS5_VABI _ZNSt6locale5facet9_RegisterEv_nid_postfix(GuestLocale::Facet* self); @@ -604,6 +629,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); @@ -669,6 +696,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); @@ -1268,6 +1297,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); @@ -1383,6 +1413,7 @@ std::int64_t APS5_VABI _ZNSt8__sce_v216_Xtime_get_ticksEv_nid_postfix(); const char* APS5_VABI _ZNSt8__sce_v213_Syserror_mapEi_nid_postfix(int value); int APS5_VABI feupdateenv_nid_postfix(const GuestFloatingPointEnvironment* environment); int APS5_VABI feholdexcept_nid_postfix(GuestFloatingPointEnvironment* environment); +extern const GuestFloatingPointEnvironment __fe_dfl_env_nid_postfix; void APS5_VABI srandomdev_nid_postfix(); double APS5_VABI _Stodx_nid_postfix(const char* text, char** end, std::int64_t pten, int* error); 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/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 4d4db4d068..33499fd85e 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -13,6 +13,7 @@ #include "general/ExportMacros.hpp" extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* funcName); +extern "C" std::uint64_t NotImplementedCalls_nid_no_patch(); extern "C" void CxaFinalize_nid_no_patch(void* dsoHandle); extern "C" bool StopAtPrefixLetter_nid_no_patch(const char* str, char** endptr, int base); extern "C" bool RejectIntegerBase_nid_no_patch(const char* str, char** endptr, int base, bool setError); @@ -27,6 +28,7 @@ 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); 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/GuestLocale.hpp b/core/libs/prx/libc/include/GuestLocale.hpp index 92ca9f13b8..ae0cb3a9ce 100644 --- a/core/libs/prx/libc/include/GuestLocale.hpp +++ b/core/libs/prx/libc/include/GuestLocale.hpp @@ -5,6 +5,8 @@ #include #include "general/VabiMacros.hpp" +struct GuestTm; + namespace GuestLocale { struct LocinfoStorage { @@ -87,11 +89,6 @@ struct NumpunctVtable { String* (APS5_VABI *truename)(String* result, const NumpunctFacet* self); }; -struct UnsupportedMethodsVtable { - FacetVtable facet; - void (APS5_VABI *methods[8])(); -}; - struct IosBase { const void* vtable; std::int64_t standardStream; @@ -106,12 +103,66 @@ struct IosBase { Implementation** locale; }; +struct Streambuf; + +struct StreambufVtable { + const void* reserved[4]; + int (APS5_VABI *overflow)(Streambuf* self, int character); +}; + +struct Streambuf { + const StreambufVtable* vtable; + const void* reserved[7]; + char** putNext; + const void* reservedCounts[3]; + char** putEnd; +}; + +struct OutputIterator { + bool failed; + Streambuf* buffer; +}; + +struct TimePutVtable { + FacetVtable facet; + OutputIterator (APS5_VABI *put)(const Facet* self, OutputIterator out, IosBase* stream, char fill, const GuestTm* time, char format, char modifier); +}; + +struct NumPutVtable { + FacetVtable facet; + OutputIterator (APS5_VABI *putBool)(const Facet* self, OutputIterator out, IosBase* stream, char fill, bool value); + OutputIterator (APS5_VABI *putLong)(const Facet* self, OutputIterator out, IosBase* stream, char fill, std::int64_t value); + OutputIterator (APS5_VABI *putUnsignedLong)(const Facet* self, OutputIterator out, IosBase* stream, char fill, std::uint64_t value); + OutputIterator (APS5_VABI *putLongLong)(const Facet* self, OutputIterator out, IosBase* stream, char fill, std::int64_t value); + OutputIterator (APS5_VABI *putUnsignedLongLong)(const Facet* self, OutputIterator out, IosBase* stream, char fill, std::uint64_t value); + OutputIterator (APS5_VABI *putDouble)(const Facet* self, OutputIterator out, IosBase* stream, char fill, double value); + OutputIterator (APS5_VABI *putLongDouble)(const Facet* self, OutputIterator out, IosBase* stream, char fill, long double value); + OutputIterator (APS5_VABI *putPointer)(const Facet* self, OutputIterator out, IosBase* stream, char fill, const void* value); +}; + +struct CtypeVtable { + FacetVtable facet; + const void* caseMethods[4]; + char (APS5_VABI *widen)(const Facet* self, char character); + const char* (APS5_VABI *widenRange)(const Facet* self, const char* first, const char* last, char* destination); +}; + +static_assert(offsetof(StreambufVtable, overflow) == 0x20); +static_assert(offsetof(Streambuf, putNext) == 0x40); +static_assert(offsetof(Streambuf, putEnd) == 0x60); +static_assert(sizeof(OutputIterator) == 0x10); +static_assert(offsetof(TimePutVtable, put) == 0x20); +static_assert(offsetof(NumPutVtable, putBool) == 0x20); +static_assert(offsetof(NumPutVtable, putPointer) == 0x58); +static_assert(offsetof(CtypeVtable, widenRange) == 0x48); static_assert(sizeof(Facet) == 0x10); static_assert(offsetof(Implementation, facets) == 0x10); static_assert(offsetof(Implementation, facetCount) == 0x18); static_assert(offsetof(Implementation, transparent) == 0x24); static_assert(offsetof(Implementation, name) == 0x28); static_assert(offsetof(IosBase, state) == 0x10); +static_assert(offsetof(IosBase, precision) == 0x1c); +static_assert(offsetof(IosBase, width) == 0x20); static_assert(offsetof(IosBase, storage) == 0x28); static_assert(offsetof(IosBase, callbacks) == 0x30); static_assert(offsetof(IosBase, locale) == 0x38); 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 2023452e25..ad07f41a7b 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -208,12 +208,44 @@ inline std::string PadFloat(const FloatSpec& spec, const std::string& prefix, co return spec.zero ? prefix + padding + body : padding + prefix + body; } -inline std::string HexFloat(const std::string& spec, char conversion, double value, std::size_t maximumLength = INT_MAX) { +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 bool plus = parsed.plus, space = parsed.space, alternate = parsed.alternate; - const std::size_t width = parsed.width; 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); @@ -230,36 +262,14 @@ inline std::string HexFloat(const std::string& spec, char conversion, double val } fraction &= (std::uint64_t{1} << 52) - 1; } - int digits = 13; - if (precision >= 0 && precision < 13) { - const int dropped = (13 - precision) * 4; - const std::uint64_t remainder = fraction & ((std::uint64_t{1} << dropped) - 1); - 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 = (bits >> 63) != 0 ? "-" : plus ? "+" : space ? " " : ""; - prefix += upper ? "0X" : "0x"; - const auto fractionalDigits = static_cast(precision > 13 ? precision : digits); - const bool point = fractionalDigits != 0 || alternate; - const std::size_t length = prefix.size() + 1 + point + fractionalDigits + suffix.size(); - if (length > maximumLength || 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 > 13) body.append(static_cast(precision - 13), '0'); - body += suffix; - return PadFloat(parsed, prefix, body); + 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 { @@ -649,8 +659,9 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo const long double value = args.Next(); 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 = DecimalFloat(spec, conversion, value, static_cast(INT_MAX - output.Count())); + } 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); 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 be7ef61900..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); } 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..b52a211209 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,37 +44,36 @@ 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) { + if (!_Stderr_nid_postfix.IsOpen()) return; const int saved = *__error_nid_postfix(); char message[128]; strerror_r_nid_postfix(saved, message, sizeof(message)); 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 053d0a8b7f..b6c54e3d2d 100644 --- a/core/libs/prx/libc/src/Filesystem.cpp +++ b/core/libs/prx/libc/src/Filesystem.cpp @@ -98,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/FloatingPoint.cpp b/core/libs/prx/libc/src/FloatingPoint.cpp index ea910f0f3b..9ba5350159 100644 --- a/core/libs/prx/libc/src/FloatingPoint.cpp +++ b/core/libs/prx/libc/src/FloatingPoint.cpp @@ -53,7 +53,7 @@ int APS5_VABI fesetround_nid_postfix(int rounding) { int APS5_VABI fetestexcept_nid_postfix(int exceptions) { std::uint16_t status; asm volatile("fnstsw %0" : "=am"(status)); - return (status | _mm_getcsr()) & static_cast(exceptions) & Exceptions; + return static_cast((status | _mm_getcsr()) & static_cast(exceptions)); } int APS5_VABI fegetexceptflag_nid_postfix(std::uint16_t* flags, int exceptions) { @@ -84,6 +84,14 @@ int APS5_VABI fesetexceptflag_nid_postfix(const std::uint16_t* flags, int except return 0; } +int APS5_VABI feraiseexcept_nid_postfix(int exceptions) { + const auto raised = static_cast(exceptions); + if ((raised & ~Exceptions) != 0) throw std::invalid_argument("feraiseexcept: unsupported exception bits"); + ReplaceFlags(raised, raised); + asm volatile("fwait"); + return 0; +} + int APS5_VABI feclearexcept_nid_postfix(int exceptions) { const auto mask = static_cast(exceptions) & Exceptions; if (mask == Exceptions) { diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index f44d577f43..1e908eee91 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -125,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; @@ -136,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; @@ -171,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); @@ -183,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') { @@ -229,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; @@ -264,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" { @@ -577,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) { @@ -593,6 +714,7 @@ int APS5_VABI snprintf_s_nid_postfix(char* buffer, size_t size, const char* form } int APS5_VABI printf_s_nid_postfix(const char* format, ...) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); const int result = LibcDetail::PrintWindows(format, args); @@ -633,6 +755,7 @@ int APS5_VABI snprintf_s_nid_postfix(char* buffer, size_t size, const char* form } int APS5_VABI printf_s_nid_postfix(const char* format, ...) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } std::va_list args; va_start(args, format); const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vfprintf(GuestStandardOutputFile_nid_no_patch(), format, args); @@ -690,4 +813,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 67a7fce10f..b4d7d13bf0 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -1,6 +1,8 @@ +#include #include "prx/libc/include/Api.hpp" #include #include +#include #include #include #include @@ -82,6 +84,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' '); @@ -170,12 +184,33 @@ std::u16string FormatWide(const char16_t* format, VaList* source, bool secure = } else value = args.Next(); if (conversion == u'o' && value == 0 && precision == 0 && spec.find('#') != std::string::npos) spec = spec.substr(0, spec.find('.')) + ".1"; + const auto flags = spec.begin() + static_cast(std::min(spec.find_first_not_of("-+ #0", 1), spec.size())); + spec.erase(std::remove_if(spec.begin() + 1, flags, [](char flag) { return flag == ' ' || flag == '+'; }), flags); 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()); @@ -361,6 +396,39 @@ 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; } diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index e84d6caafc..9c68872826 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -541,11 +541,15 @@ int SetWorkingDirectory(WorkingDirectory& state, const std::filesystem::path& re } } -extern "C" int CopyGuestPath_nid_no_patch(const char* source, std::string& destination) { - constexpr std::size_t maximum = 1024; +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::array bytes{}; + 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; @@ -566,6 +570,10 @@ extern "C" int CopyGuestPath_nid_no_patch(const char* source, std::string& desti 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; @@ -853,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; } @@ -982,6 +996,17 @@ extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor) { } #endif +namespace { + +std::atomic notImplementedCalls{0}; + +} + extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* funcName) { + notImplementedCalls.fetch_add(1, std::memory_order_relaxed); throw std::runtime_error(std::string(funcName) + " not implemented"); } + +extern "C" std::uint64_t NotImplementedCalls_nid_no_patch() { + return notImplementedCalls.load(std::memory_order_relaxed); +} 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/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index a838c65929..0a302dec63 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -1,5 +1,7 @@ #include "prx/libc/include/Api.hpp" #include +#include +#include #include #include #include @@ -25,6 +27,12 @@ #include "prx/libc/include/GuestLocale.hpp" #include "SceTypes.hpp" +extern "C" { +extern std::uint64_t _ZNSt5ctypeIcE2idE_nid_postfix; +extern std::uint64_t _ZNSt8numpunctIcE2idE_nid_postfix; +extern std::int32_t _ZNSt6locale2id7_Id_cntE_nid_postfix; +} + namespace { std::mutex g_localeInitMutex; @@ -281,16 +289,230 @@ GuestLocale::String* APS5_VABI NumpunctTruename(GuestLocale::String* result, con const GuestLocale::NumpunctVtable g_numpunctVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, NumpunctDecimalPoint, NumpunctThousandsSep, NumpunctGrouping, NumpunctFalsename, NumpunctTruename}; -void APS5_VABI NumPutMethodUnsupported() { - NotImplemented_nid_no_patch("num_put::do_put"); +void PutCharacter(GuestLocale::OutputIterator& out, char character) { + GuestLocale::Streambuf* buffer = out.buffer; + if (buffer == nullptr) { + out.failed = true; + return; + } + char* next = *buffer->putNext; + if (next != nullptr && static_cast(static_cast(reinterpret_cast(*buffer->putEnd) - reinterpret_cast(next))) > 0) { + *buffer->putNext = next + 1; + *next = character; + return; + } + if (buffer->vtable->overflow(buffer, static_cast(character)) == -1) out.failed = true; } -void APS5_VABI TimePutMethodUnsupported() { - NotImplemented_nid_no_patch("time_put::do_put"); +GuestLocale::OutputIterator APS5_VABI TimePutDoPut(const GuestLocale::Facet*, GuestLocale::OutputIterator out, GuestLocale::IosBase*, char, const GuestTm* time, char format, char modifier) { + const char pattern[] = {'!', '%', modifier != 0 ? modifier : format, modifier != 0 ? format : '\0', '\0'}; + std::string text; + std::size_t length = 0; + for (std::size_t size = 16; length == 0; size *= 2) { + text.resize(size); + length = strftime_nid_postfix(text.data(), size, pattern, time); + } + for (std::size_t index = 1; index < length; ++index) PutCharacter(out, text[index]); + return out; } -const GuestLocale::UnsupportedMethodsVtable g_numPutVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, {NumPutMethodUnsupported, NumPutMethodUnsupported, NumPutMethodUnsupported, NumPutMethodUnsupported, NumPutMethodUnsupported, NumPutMethodUnsupported, NumPutMethodUnsupported, NumPutMethodUnsupported}}; -const GuestLocale::UnsupportedMethodsVtable g_timePutVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, {TimePutMethodUnsupported, TimePutMethodUnsupported, TimePutMethodUnsupported, TimePutMethodUnsupported, TimePutMethodUnsupported, TimePutMethodUnsupported, TimePutMethodUnsupported, TimePutMethodUnsupported}}; +constexpr std::int32_t IosLeft = 0x2; +constexpr std::int32_t IosInternal = 0x8; +constexpr std::int32_t IosAdjustField = 0xe; +constexpr std::int32_t IosOct = 0x20; +constexpr std::int32_t IosHex = 0x40; +constexpr std::int32_t IosBaseField = 0x70; +constexpr std::int32_t IosShowBase = 0x80; +constexpr std::int32_t IosShowPoint = 0x100; +constexpr std::int32_t IosUppercase = 0x200; +constexpr std::int32_t IosShowPos = 0x400; +constexpr std::int32_t IosScientific = 0x800; +constexpr std::int32_t IosFixed = 0x1000; +constexpr std::int32_t IosHexFloat = 0x1800; +constexpr std::int32_t IosBoolAlpha = 0x8000; + +const GuestLocale::Facet* FindFacet(const GuestLocale::IosBase* stream, std::uint64_t& id) { + std::atomic_ref slot(id); + if (slot.load() == 0) { + _Locksyslock_nid_postfix(); + if (slot.load() == 0) slot.store(static_cast(++_ZNSt6locale2id7_Id_cntE_nid_postfix)); + _Unlocksyslock_nid_postfix(); + } + const std::uint64_t index = slot.load(); + const GuestLocale::Implementation* locale = *stream->locale; + const GuestLocale::Facet* facet = index < locale->facetCount ? locale->facets[index] : nullptr; + if (facet != nullptr || !locale->transparent) return facet; + const GuestLocale::Implementation* global = _ZNSt6locale16_GetgloballocaleEv_nid_postfix(); + return index < global->facetCount ? global->facets[index] : nullptr; +} + +const GuestLocale::Facet* StreamNumpunct(const GuestLocale::IosBase* stream) { + if (const GuestLocale::Facet* facet = FindFacet(stream, _ZNSt8numpunctIcE2idE_nid_postfix)) return facet; + static GuestLocale::Facet* const fallback = [] { + GuestLocale::Facet* created = nullptr; + _ZNSt8numpunctIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&created, nullptr); + return created; + }(); + return fallback; +} + +const GuestLocale::NumpunctVtable& Punctuation(const GuestLocale::Facet* facet) { + return *reinterpret_cast(facet->vtable); +} + +const GuestLocale::NumpunctFacet* AsNumpunct(const GuestLocale::Facet* facet) { + return reinterpret_cast(facet); +} + +std::string TakeString(GuestLocale::String* (APS5_VABI* method)(GuestLocale::String*, const GuestLocale::NumpunctFacet*), const GuestLocale::Facet* facet) { + GuestLocale::String value{}; + method(&value, AsNumpunct(facet)); + const bool onHeap = value.capacity > GuestStringInlineCapacity; + std::string text(onHeap ? value.pointer : value.buffer, value.size); + if (onHeap) _ZdlPv_nid_postfix(value.capacity + 1 < GuestStringLargeAllocation ? static_cast(value.pointer) : reinterpret_cast(value.pointer)[-1]); + return text; +} + +std::string Widen(const GuestLocale::IosBase* stream, const char* text, std::size_t count) { + std::string result(text, count); + if (const GuestLocale::Facet* ctype = FindFacet(stream, _ZNSt5ctypeIcE2idE_nid_postfix)) reinterpret_cast(ctype->vtable)->widenRange(ctype, text, text + count, result.data()); + return result; +} + +bool HexPrefix(const char* text, std::size_t prefix, std::size_t count) { + return prefix + 2 <= count && text[prefix] == '0' && (text[prefix + 1] == 'x' || text[prefix + 1] == 'X'); +} + +std::size_t SignPrefix(const char* text, std::size_t count) { + return count > 0 && (text[0] == '+' || text[0] == '-') ? 1 : 0; +} + +void InsertSeparators(std::string& text, const std::string& grouping, char separator, std::size_t offset, std::size_t prefix) { + const char* group = grouping.c_str(); + while (static_cast(*group) != CHAR_MAX && static_cast(*group) > 0 && static_cast(*group) < offset - prefix) { + offset -= static_cast(*group); + text.insert(offset, 1, separator); + if (static_cast(group[1]) > 0) ++group; + } +} + +GuestLocale::OutputIterator PutField(GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, const std::string& text, std::size_t prefix) { + const std::int32_t width = stream->width; + std::size_t fillCount = width <= 0 || static_cast(width) <= text.size() ? 0 : static_cast(width) - text.size(); + const std::int32_t adjust = stream->flags & IosAdjustField; + if (adjust != IosLeft && adjust != IosInternal) { + for (; fillCount != 0; --fillCount) PutCharacter(out, fill); + } + for (std::size_t index = 0; index < prefix; ++index) PutCharacter(out, text[index]); + if (adjust == IosInternal) { + for (; fillCount != 0; --fillCount) PutCharacter(out, fill); + } + for (std::size_t index = prefix; index < text.size(); ++index) PutCharacter(out, text[index]); + stream->width = 0; + for (; fillCount != 0; --fillCount) PutCharacter(out, fill); + return out; +} + +GuestLocale::OutputIterator PutInteger(GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, const char* digits, std::size_t count) { + std::size_t prefix = SignPrefix(digits, count); + if ((stream->flags & IosBaseField) == IosHex && HexPrefix(digits, prefix, count)) prefix += 2; + std::string text = Widen(stream, digits, count); + const GuestLocale::Facet* punct = StreamNumpunct(stream); + const std::string grouping = TakeString(Punctuation(punct).grouping, punct); + if (static_cast(grouping[0]) != CHAR_MAX && static_cast(grouping[0]) > 0) InsertSeparators(text, grouping, Punctuation(punct).thousandsSep(AsNumpunct(punct)), count, prefix); + return PutField(out, stream, fill, text, prefix); +} + +GuestLocale::OutputIterator PutFloating(GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, const char* digits, std::size_t count) { + const std::size_t sign = SignPrefix(digits, count); + const std::size_t prefix = sign != 0 ? sign : HexPrefix(digits, 0, count) ? 2 : 0; + const bool hex = (stream->flags & IosHexFloat) == IosHexFloat; + const std::size_t exponent = std::strcspn(digits, hex ? "pP" : "eE"); + const char point[2] = {localeconv_nid_postfix()->decimal_point[0], '\0'}; + const std::size_t pointOffset = std::strcspn(digits, point); + std::string text = Widen(stream, digits, count); + const GuestLocale::Facet* punct = StreamNumpunct(stream); + const std::string grouping = TakeString(Punctuation(punct).grouping, punct); + const char separator = Punctuation(punct).thousandsSep(AsNumpunct(punct)); + std::size_t offset = exponent; + if (pointOffset != count) { + offset = pointOffset; + text[pointOffset] = Punctuation(punct).decimalPoint(AsNumpunct(punct)); + } + const char lead = digits[prefix]; + if (lead != 'I' && lead != 'N' && lead != 'i' && lead != 'n') InsertSeparators(text, grouping, separator, offset, prefix); + return PutField(out, stream, fill, text, prefix); +} + +template GuestLocale::OutputIterator PutFormattedInteger(GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, const char* qualifier, char decimal, Value value) { + const std::int32_t flags = stream->flags; + const std::int32_t base = flags & IosBaseField; + const char conversion = base == IosOct ? 'o' : base != IosHex ? decimal : (flags & IosUppercase) != 0 ? 'X' : 'x'; + const std::string format = std::string("%") + ((flags & IosShowPos) != 0 ? "+" : "") + ((flags & IosShowBase) != 0 ? "#" : "") + qualifier + conversion; + char digits[64]; + const int count = snprintf_nid_postfix(digits, sizeof(digits), format.c_str(), value); + return PutInteger(out, stream, fill, digits, static_cast(count)); +} + +template GuestLocale::OutputIterator PutFormattedFloating(GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, const char* qualifier, Value value) { + const std::int32_t flags = stream->flags; + const std::int32_t field = flags & IosHexFloat; + const std::int32_t precision = field == IosHexFloat ? 6 : stream->precision; + std::int64_t size = precision; + if (field == IosFixed && std::fabs(value) > Value(1e10)) { + int exponent = 0; + std::frexp(value, &exponent); + size += static_cast(std::abs(exponent)) * 30103 / 100000; + } + size += 50; + const char conversion = field == IosFixed ? 'f' : field == IosHexFloat ? 'a' : field == IosScientific ? 'e' : 'g'; + const bool upper = (flags & IosUppercase) != 0 && field != IosFixed; + const std::string format = std::string("%") + ((flags & IosShowPos) != 0 ? "+" : "") + ((flags & IosShowPoint) != 0 ? "#" : "") + (field != IosHexFloat ? ".*" : "") + qualifier + static_cast(upper ? conversion - 'a' + 'A' : conversion); + if (size <= 0) throw std::invalid_argument("num_put::do_put: the precision leaves no room for the converted value"); + std::string digits(static_cast(size), '\0'); + const int count = snprintf_nid_postfix(digits.data(), digits.size(), format.c_str(), precision, value); + if (count < 0 || count >= size) throw std::invalid_argument("num_put::do_put: the precision leaves no room for the converted value"); + return PutFloating(out, stream, fill, digits.c_str(), static_cast(count)); +} + +GuestLocale::OutputIterator APS5_VABI NumPutBool(const GuestLocale::Facet* self, GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, bool value) { + if ((stream->flags & IosBoolAlpha) == 0) return reinterpret_cast(self->vtable)->putLong(self, out, stream, fill, value ? 1 : 0); + const GuestLocale::Facet* punct = StreamNumpunct(stream); + return PutField(out, stream, fill, TakeString(value ? Punctuation(punct).truename : Punctuation(punct).falsename, punct), 0); +} + +GuestLocale::OutputIterator APS5_VABI NumPutLong(const GuestLocale::Facet*, GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, std::int64_t value) { + return PutFormattedInteger(out, stream, fill, "l", 'd', value); +} + +GuestLocale::OutputIterator APS5_VABI NumPutUnsignedLong(const GuestLocale::Facet*, GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, std::uint64_t value) { + return PutFormattedInteger(out, stream, fill, "l", 'u', value); +} + +GuestLocale::OutputIterator APS5_VABI NumPutLongLong(const GuestLocale::Facet*, GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, std::int64_t value) { + return PutFormattedInteger(out, stream, fill, "ll", 'd', value); +} + +GuestLocale::OutputIterator APS5_VABI NumPutUnsignedLongLong(const GuestLocale::Facet*, GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, std::uint64_t value) { + return PutFormattedInteger(out, stream, fill, "ll", 'u', value); +} + +GuestLocale::OutputIterator APS5_VABI NumPutDouble(const GuestLocale::Facet*, GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, double value) { + return PutFormattedFloating(out, stream, fill, "", value); +} + +GuestLocale::OutputIterator APS5_VABI NumPutLongDouble(const GuestLocale::Facet*, GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, long double value) { + return PutFormattedFloating(out, stream, fill, "L", value); +} + +GuestLocale::OutputIterator APS5_VABI NumPutPointer(const GuestLocale::Facet*, GuestLocale::OutputIterator out, GuestLocale::IosBase* stream, char fill, const void* value) { + char digits[64]; + const int count = snprintf_nid_postfix(digits, sizeof(digits), "%p", value); + return PutInteger(out, stream, fill, digits, static_cast(count)); +} + +const GuestLocale::NumPutVtable g_numPutVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, NumPutBool, NumPutLong, NumPutUnsignedLong, NumPutLongLong, NumPutUnsignedLongLong, NumPutDouble, NumPutLongDouble, NumPutPointer}; +const GuestLocale::TimePutVtable g_timePutVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, TimePutDoPut}; constexpr std::size_t GuestTimePutFacetSize = 0xb8; @@ -301,6 +523,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 +551,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 +572,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); } @@ -370,7 +593,17 @@ std::uint64_t _ZNSt5ctypeIcE2idE_nid_postfix = 0; std::uint64_t _ZNSt5ctypeIwE2idE_nid_postfix = 0; std::uint64_t _ZNSt7collateIwE2idE_nid_postfix = 0; std::uint64_t _ZNSt7collateIcE2idE_nid_postfix = 0; -std::uintptr_t _ZTVSt7collateIcE_nid_postfix[9] {}; +std::uintptr_t _ZTVSt7collateIcE_nid_postfix[9] { + 0, + 0, + reinterpret_cast(&DestroyCollate), + reinterpret_cast(&DeleteCollate), + reinterpret_cast(&RetainFacet), + reinterpret_cast(&ReleaseFacet), + reinterpret_cast(&CollateCompare), + reinterpret_cast(&CollateTransform), + reinterpret_cast(&CollateHash), +}; std::uint64_t _ZNSt7codecvtIcc9_MbstatetE2idE_nid_postfix = 0; std::uintptr_t _ZTVSt7codecvtIcc9_MbstatetE_nid_postfix[16] {}; std::uintptr_t _ZTVSt7codecvtIwc9_MbstatetE_nid_postfix[13] {}; @@ -384,6 +617,8 @@ std::uintptr_t _ZTVSt7num_putIwSt19ostreambuf_iteratorIwSt11char_traitsIwEEE_nid 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 {}; @@ -409,12 +644,22 @@ GuestLocale::IosBase* APS5_VABI SetStreamWidth(GuestLocale::IosBase* stream, std return stream; } +GuestLocale::IosBase* APS5_VABI SetStreamPrecision(GuestLocale::IosBase* stream, std::int32_t precision) { + if (stream == nullptr) throw std::invalid_argument("setprecision: null stream"); + stream->precision = precision; + return stream; +} + static_assert(sizeof(StreamManipulator) == 0x10); StreamManipulator APS5_VABI _ZSt4setwi_nid_postfix(std::int32_t width) { return {&SetStreamWidth, width}; } +StreamManipulator APS5_VABI _ZSt12setprecisioni_nid_postfix(std::int32_t precision) { + return {&SetStreamPrecision, precision}; +} + void APS5_VABI _ZNSt8ios_base7_AddstdEPS__nid_postfix(GuestLocale::IosBase* self) { if (self == nullptr) throw std::invalid_argument("ios_base _Addstd: null stream"); std::lock_guard lock(g_standardStreamMutex); diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index e635b0b1e1..305b915f41 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))); } @@ -396,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) { @@ -421,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); } @@ -1042,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)); @@ -1249,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; } @@ -1266,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); } @@ -1433,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(); } @@ -1477,6 +1626,8 @@ int APS5_VABI feholdexcept_nid_postfix(GuestFloatingPointEnvironment* environmen return 0; } +const GuestFloatingPointEnvironment __fe_dfl_env_nid_postfix {0xffff037fu, 0xffff0000u, 0xffffffffu, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}, 0x9fc0u}; + union SonyDconst { unsigned short _Word[8]; float _Float; diff --git a/core/libs/prx/libc/src/Random.cpp b/core/libs/prx/libc/src/Random.cpp index 67bd066466..92d012032e 100644 --- a/core/libs/prx/libc/src/Random.cpp +++ b/core/libs/prx/libc/src/Random.cpp @@ -194,11 +194,11 @@ void APS5_VABI srandomdev_nid_postfix() { } char* APS5_VABI initstate_nid_postfix(std::uint64_t seed, char* state, std::int64_t size) { + std::lock_guard lock(g_randomLock); + SaveTypeWordLocked(); if (size < MinimumStateBytes[0]) return nullptr; if (state == nullptr) throw std::invalid_argument("initstate: null state"); - std::lock_guard lock(g_randomLock); char* previous = g_random.table; - SaveTypeWordLocked(); int type = MaxTypes - 1; while (size < MinimumStateBytes[static_cast(type)]) --type; g_random = {state, type, Degrees[static_cast(type)], Separations[static_cast(type)], 0, 0}; @@ -214,7 +214,6 @@ char* APS5_VABI setstate_nid_postfix(char* state) { const int type = static_cast(word % MaxTypes); const std::uint32_t rear = word / MaxTypes; const int degree = Degrees[static_cast(type)]; - if (type != LinearCongruentialType && rear >= static_cast(degree)) return nullptr; char* previous = g_random.table; SaveTypeWordLocked(); const int separation = Separations[static_cast(type)]; diff --git a/core/libs/prx/libc/src/Rename.cpp b/core/libs/prx/libc/src/Rename.cpp index 8c41ae277f..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; @@ -131,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/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/StreamPurge.cpp b/core/libs/prx/libc/src/StreamPurge.cpp new file mode 100644 index 0000000000..eff04b3442 --- /dev/null +++ b/core/libs/prx/libc/src/StreamPurge.cpp @@ -0,0 +1,38 @@ +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#if defined(__linux__) +#include +#endif + +namespace { + +class LockedStream { + FileStream* stream; + +public: + explicit LockedStream(FileStream* value) : stream(value) { flockfile_nid_postfix(stream); } + ~LockedStream() { funlockfile_nid_postfix(stream); } +}; + +} + +extern "C" int APS5_VABI fpurge_nid_postfix(FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } + auto* native = GetStreamHandle(stream); + const LockedStream lock(stream); +#ifdef _WIN32 + auto* state = reinterpret_cast(native); + state->pointer = state->base; + state->count = 0; +#elif defined(__linux__) + ::__fpurge(native); +#else + if (::fpurge(native) != 0) return -1; +#endif + stream->GuestState().position = stream->GuestState().buffer; + stream->SyncUnlockedStatus(); + return 0; +} diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 18d1df74bc..508da2aa04 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -487,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/Time.cpp b/core/libs/prx/libc/src/Time.cpp index 14a1b8bc68..189bde0aa5 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -244,10 +244,6 @@ constexpr const char* MONTH_FULL_NAMES[] = {"January", "February", "March", "Apr "October", "November", "December"}; constexpr int YEAR_BASE = 1900; -bool isLeapYearSum(int year, int base) { - return isLeapYear(year % 400 + base % 400); -} - struct FormatBuffer { char* cursor; char* limit; @@ -287,32 +283,44 @@ int twoDigitYear(std::int64_t year) { return static_cast((year % 100 + 100) % 100); } -std::int64_t weekOfYear(const GuestTm& time, int shift) { - return (static_cast(time.tm_yday) + 7 - (static_cast(time.tm_wday) + shift) % 7) / 7; +int wrapped(std::int64_t value) { + return static_cast(static_cast(value)); +} + +int weekOfYear(const GuestTm& time, int shift) { + return wrapped(static_cast(time.tm_yday) + 7 - wrapped(static_cast(time.tm_wday) + shift) % 7) / 7; } struct IsoWeek { - std::int64_t year; + int year; int week; }; +bool isoLeapYear(int year) { + return year % 4 == 0 && (year % 100 != 0 || year / 400 == 100 || year / 400 == -300); +} + +bool midweek(int weekday) { + return weekday >= 2 && weekday <= 4; +} + 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; - std::int64_t yday = time.tm_yday; - for (;;) { - const int length = isLeapYearSum(time.tm_year, base) ? 366 : 365; - 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) 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; - } + const int leap = isoLeapYear(time.tm_year) ? 1 : 0; + const int span = wrapped(static_cast(time.tm_wday) - time.tm_yday); + const int firstWeekday = wrapped(static_cast(span) + 371) % 7; + const int nextFirstWeekday = (leap + firstWeekday + 365) % 7; + const int nextYear = wrapped(static_cast(time.tm_year) + 1); + if (midweek(nextFirstWeekday) && wrapped(static_cast(time.tm_yday) - leap) >= 366 - nextFirstWeekday) return {nextYear, 1}; + const int mondayWeekday = wrapped(static_cast(time.tm_wday) + 6) % 7; + const int week = wrapped(static_cast(time.tm_yday) - mondayWeekday + 7) / 7 + (midweek(firstWeekday) ? 1 : 0); + if (week < 0) return {nextYear, 1}; + if (week > 0) return {time.tm_year, week}; + const int previousYear = wrapped(static_cast(time.tm_year) - 1); + const int previousLeap = isoLeapYear(previousYear) ? 1 : 0; + const int previousFirstWeekday = wrapped(static_cast(span) + 13 - previousLeap) % 7; + if (midweek((previousLeap + previousFirstWeekday + 365) % 7)) return {previousYear, 0}; + const int lastWeekday = wrapped(static_cast(span) + 13) % 7; + return {previousYear, (372 + previousLeap - lastWeekday) / 7 + (midweek(previousFirstWeekday) ? 1 : 0)}; } void formatTime(FormatBuffer& buffer, const char* format, const GuestTm& time); @@ -342,7 +350,7 @@ bool addConversion(FormatBuffer& buffer, char conversion, const GuestTm& time) { 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 'G': addNumber(buffer, static_cast(isoWeek(time).year) + YEAR_BASE, 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; @@ -604,7 +612,7 @@ char* APS5_VABI asctime_nid_postfix(const GuestTm* timeptr) { char* APS5_VABI ctime_nid_postfix(const int64_t* timer) { if (const GuestTm* local = localtime_nid_postfix(timer)) return asctime_nid_postfix(local); - static char unknown[26]; + static thread_local char unknown[26]; std::memcpy(unknown, "??? ??? ?? ??:??:?? ????\n", sizeof(unknown)); errno = EINVAL; return unknown; 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/StandardExceptions.cpp b/core/libs/prx/libc/src/exception/StandardExceptions.cpp index b009e10648..83ffdb0b78 100644 --- a/core/libs/prx/libc/src/exception/StandardExceptions.cpp +++ b/core/libs/prx/libc/src/exception/StandardExceptions.cpp @@ -508,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 694896a3b0..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 @@ -113,6 +124,10 @@ void StartCallCountWriter() { 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 @@ -128,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); @@ -147,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; @@ -167,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) { @@ -198,6 +267,7 @@ void SnapshotModules() { }, map); g_modules.store(map, std::memory_order_release); } +#endif class ReportLine { public: @@ -370,12 +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; @@ -475,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 1951f94ba6..c4f1acc4bc 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -127,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}) @@ -524,6 +538,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_barrier PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_setname_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSetname.cpp) + target_include_directories(guest_pthread_setname_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_setname_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_setname_tests) + add_test(NAME guest_pthread_setname COMMAND guest_pthread_setname_tests) + set_tests_properties(guest_pthread_setname PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_setname PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_kernel_is_stack_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelIsStack.cpp) target_include_directories(guest_kernel_is_stack_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_kernel_is_stack_tests PRIVATE libkernel libc) @@ -566,15 +590,17 @@ 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:$") + if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + 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() 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}) @@ -743,6 +769,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) @@ -1215,7 +1251,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) @@ -1577,7 +1613,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}) @@ -1781,6 +1817,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test_executable(guest_system_flexible_memory_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemFlexibleMemory.cpp) + target_include_directories(guest_system_flexible_memory_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_system_flexible_memory_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_system_flexible_memory_tests) + add_test(NAME guest_system_flexible_memory COMMAND guest_system_flexible_memory_tests) + set_tests_properties(guest_system_flexible_memory PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_system_flexible_memory PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_page_table_stats_tests ${LIBS_INCLUDE_DIR}/tests/GuestPageTableStats.cpp) target_include_directories(guest_page_table_stats_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_page_table_stats_tests PRIVATE libkernel libc) @@ -1811,6 +1857,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) @@ -1958,6 +2011,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:$") @@ -1994,6 +2057,11 @@ if(BUILD_TESTING) add_test(NAME guest_raise_in_system_call COMMAND guest_raise_in_system_call_tests) set_tests_properties(guest_raise_in_system_call PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + foreach(delay IN ITEMS raise return) + add_test(NAME guest_raise_in_system_call_delay_${delay} COMMAND guest_raise_in_system_call_tests --delay-${delay}) + set_tests_properties(guest_raise_in_system_call_delay_${delay} PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endforeach() add_test_executable(guest_raise_in_host_code_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseInHostCode.cpp) target_include_directories(guest_raise_in_host_code_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_raise_in_host_code_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 93101cb207..75fe04fd67 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -567,6 +567,18 @@ int APS5_VABI sceKernelBatchMap2(KernelBatchMapEntry* entries, int num_entries, return result; } +int32_t APS5_VABI sceKernelMapNamedSystemFlexibleMemory(void** addr_in_out, size_t len, int prot, int flags, const char* name) { + constexpr size_t MaximumNameLength = 32; + if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; + if (name == nullptr) throw std::runtime_error("sceKernelMapNamedSystemFlexibleMemory: null name"); + if (strnlen(name, MaximumNameLength) >= MaximumNameLength) throw std::runtime_error("sceKernelMapNamedSystemFlexibleMemory: name of 32 bytes or more"); + const int result = DoMapAnon(addr_in_out, len, prot, flags); + if (result != 0) return result; + _releaseFlexible(reinterpret_cast(*addr_in_out), len); + sceKernelSetVirtualRangeName(*addr_in_out, len, name); + return 0; +} + } bool GuestRangeMapped(std::uintptr_t start, std::uintptr_t end) { 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 de05e2e1cb..1bee0fd94a 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -358,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/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..5c24a9ff80 100644 --- a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp +++ b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp @@ -4,18 +4,27 @@ #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); +extern "C" int ChmodNative_nid_no_patch(const std::filesystem::path& native, int mode); 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..337c823682 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -45,6 +45,7 @@ constexpr int GuestEfault = 14; constexpr int GuestEinval = 22; constexpr int GuestEnotty = 25; constexpr int GuestEagain = 35; +constexpr int GuestEopnotsupp = 45; constexpr int GuestEnolck = 77; constexpr int GuestEoverflow = 84; @@ -378,7 +379,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 +401,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); @@ -489,21 +490,20 @@ FcntlArgument FcntlArgumentOf(int command) { int Fcntl(int fd, int command, std::intptr_t argument) { switch (command) { - case DuplicateCommand: - case DuplicateCloseOnExec: { + case DuplicateCommand: { if (fd < 0) return Fail(GuestEbadf); if (static_cast(argument) < 0) return Fail(GuestEinval); if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); - const int duplicate = DuplicateFrom(fd, static_cast(argument)); - if (duplicate >= 0 && command == DuplicateCloseOnExec) SetCloseOnExec(duplicate, true); - return duplicate; + return DuplicateFrom(fd, static_cast(argument)); } + case DuplicateCloseOnExec: + if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); + return Fail(GuestEopnotsupp); case DuplicateToCommand: - case DuplicateToCloseOnExec: { - const int duplicate = dup2_nid_postfix(fd, static_cast(argument)); - if (duplicate >= 0 && command == DuplicateToCloseOnExec) SetCloseOnExec(duplicate, true); - return duplicate; - } + return dup2_nid_postfix(fd, static_cast(argument)); + case DuplicateToCloseOnExec: + if (fd < 0 || !Classify(fd, "fcntl")) return Fail(GuestEbadf); + return DuplicateFrom(fd, 0); case GetDescriptorFlags: { if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); std::lock_guard lock(g_mutex); @@ -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 537336c50b..9390adc7d1 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" @@ -84,6 +85,7 @@ static int NativeWrite(int fd, const void* buf, std::size_t n) { } const int result = ::_write(fd, buf, static_cast(n)); if (result < 0 && errno == ENOSPC && _doserrno == 0 && ::GetFileType(reinterpret_cast(::_get_osfhandle(fd))) == FILE_TYPE_PIPE) errno = EAGAIN; + if (result < 0 && (_doserrno == ERROR_NO_DATA || _doserrno == ERROR_BROKEN_PIPE) && ::GetFileType(reinterpret_cast(::_get_osfhandle(fd))) == FILE_TYPE_PIPE) errno = EPIPE; return result; } extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); @@ -201,12 +203,24 @@ 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; @@ -312,9 +326,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)); @@ -323,6 +337,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; @@ -342,6 +357,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; @@ -351,6 +367,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); @@ -375,12 +392,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; @@ -455,7 +470,20 @@ int APS5_VABI sceKernelUnlink(const char* path) { if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); constexpr int GuestEperm = 1; std::error_code error; - if (std::filesystem::is_directory(std::filesystem::symlink_status(native, error))) return SceErrorFromErrno(GuestEperm); + if (std::filesystem::is_directory(std::filesystem::symlink_status(native, error))) { +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { + std::string junctionTarget; + if (File::WindowsSymbolicLink::ReadJunction(native, junctionTarget, "unlink")) { + if (RemoveDirectoryW(native.c_str()) == 0) return SceErrorFromErrno(GuestEperm); + RecordWrittenPath_nid_no_patch(native); + return 0; + } + } +#endif + return SceErrorFromErrno(GuestEperm); + } if (NativeUnlink(native) != 0) { #ifdef _WIN32 if (NativeSymbolicLinkLoop(native.parent_path())) return SCE_KERNEL_ERROR_ELOOP; diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index bf22e21d02..ec08e5319c 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -204,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; } @@ -337,7 +338,10 @@ static std::int64_t NativeTransferBuffer(int descriptor, void* buf, std::size_t if (appended != -2) return appended; } } - return write ? ::_write(descriptor, buf, static_cast(nbytes)) : ::_read(descriptor, buf, static_cast(nbytes)); + if (!write) return ::_read(descriptor, buf, static_cast(nbytes)); + const int result = ::_write(descriptor, buf, static_cast(nbytes)); + if (result < 0 && (_doserrno == ERROR_NO_DATA || _doserrno == ERROR_BROKEN_PIPE) && ::GetFileType(reinterpret_cast(::_get_osfhandle(descriptor))) == FILE_TYPE_PIPE) errno = EPIPE; + return result; } static std::int64_t NativeTransfer(int descriptor, void* buf, std::size_t nbytes, bool write) { const auto positioned = File::LockPositionedTransfer(descriptor); @@ -462,10 +466,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; } @@ -655,6 +666,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; @@ -662,6 +674,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; @@ -710,6 +731,10 @@ int APS5_VABI chmod_nid_postfix(const char* path, int mode) { return 0; } +int ChmodNative_nid_no_patch(const std::filesystem::path& native, int mode) { + return NativeChmod(native, mode); +} + int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { std::string copy; if (const int error = PathError(path, copy)) return PosixFailure(error); @@ -722,6 +747,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)) { @@ -757,8 +783,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; @@ -801,12 +827,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); @@ -822,8 +848,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; @@ -863,6 +889,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); @@ -874,6 +902,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 @@ -905,6 +934,7 @@ 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"); @@ -936,6 +966,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); } @@ -1013,8 +1044,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); @@ -1026,9 +1058,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); @@ -1127,6 +1161,7 @@ int APS5_VABI sceKernelFstat(int d, FileStat* sb) { } int APS5_VABI sceKernelFsync(int fd) { + if (File::IsVirtualDirectoryDescriptor(fd)) return SceErrorFromErrno(GUEST_EISDIR); if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); File::RefuseArchivedFile(fd, "fsync"); #ifdef _WIN32 @@ -1138,6 +1173,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 @@ -1216,12 +1252,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); @@ -1323,6 +1362,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; } @@ -1349,7 +1393,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); @@ -1361,8 +1406,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); @@ -1370,14 +1417,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); @@ -1391,6 +1440,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) { @@ -1431,25 +1481,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); @@ -1460,14 +1515,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(); @@ -1484,6 +1542,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(); @@ -1491,6 +1550,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; @@ -1499,10 +1559,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); @@ -1513,11 +1574,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; @@ -1541,8 +1604,6 @@ 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) { @@ -1556,6 +1617,13 @@ int APS5_VABI sceKernelRmdir(const char* path) { std::error_code error; if (std::filesystem::is_symlink(native, error)) return SceErrorFromErrno(GUEST_ENOTDIR); if (const int failure = PathLookupFailure(error)) return failure; +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0 && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { + std::string junctionTarget; + if (File::WindowsSymbolicLink::ReadJunction(native, junctionTarget, "rmdir")) return SceErrorFromErrno(GUEST_ENOTDIR); + } +#endif if (!std::filesystem::is_directory(native, error)) { #ifdef _WIN32 if (File::WindowsSymbolicLinkLoop(WithoutTrailingSeparator(native).parent_path())) return SceKernelError(GUEST_ELOOP); @@ -1592,6 +1660,7 @@ 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"); @@ -1669,11 +1738,12 @@ int APS5_VABI lutimes_nid_postfix(const char* path, const KernelTimeval* times) 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)); + if (NativeUtimes(native, times, false) != 0) return PosixResult(SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno)); return 0; } 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/Pthread/Posix/Attr.cpp b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp index 1dae3e0f74..a9c3f61aed 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp @@ -32,6 +32,7 @@ int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr) 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; diff --git a/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp index 9d98ca6274..87ededece4 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp @@ -91,4 +91,17 @@ int APS5_VABI pthread_barrier_destroy_nid_postfix(PthreadBarrierPrivate** barrie return 0; } +int APS5_VABI pthread_barrier_setname_np_nid_postfix(PthreadBarrierPrivate** barrier, const char* name) { + if (!barrier) throw std::invalid_argument("pthread_barrier_setname_np: null barrier"); + if (!*barrier) throw std::invalid_argument("pthread_barrier_setname_np: barrier is not initialized or was destroyed"); + auto& value = **barrier; + std::lock_guard lock(value.mutex); + if (value.named) return 35; + if (name) { + value.name = name; + value.named = true; + } + return 0; +} + } 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/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 097d609562..e34a09f3bf 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -47,6 +47,8 @@ struct PthreadMutexPrivate { int _count; int _spinloops = 0; int _yieldloops = 0; + std::string _name; + bool _named = false; PthreadMutexPrivate() : _type(MutexType::ErrorCheck), _owner(std::thread::id{}), _count(0) {} @@ -66,6 +68,8 @@ struct PthreadRwlockPrivate { std::atomic _writer; std::atomic _readers{0}; int _waitingWriters = 0; + std::string _name; + bool _named = false; }; struct PthreadCondattrPrivate { @@ -134,6 +138,7 @@ struct PthreadPrivate { 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; diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index 9b7cb2e521..7a7d3371af 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -97,7 +97,7 @@ 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; @@ -125,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 34b88bfd80..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) { @@ -177,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 84eee73602..8525c3d088 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -1,8 +1,11 @@ +#include "prx/libkernel/include/Api.hpp" #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 +13,7 @@ #include #include #include +#include namespace { @@ -17,6 +21,8 @@ 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; +constexpr int posixAgain = 35; std::mutex& InitializationMutex() { static std::mutex mutex; return mutex; @@ -137,6 +143,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; @@ -267,4 +284,20 @@ int APS5_VABI scePthreadMutexSetyieldloops(PthreadMutex* mutex, int count) { return 0; } +int APS5_VABI pthread_mutex_setname_np_nid_postfix(PthreadMutex* mutex, const char* name) { + if (!mutex) + throw std::invalid_argument("pthread_mutex_setname_np: null mutex"); + std::lock_guard lock(InitializationMutex()); + const auto current = std::atomic_ref(*mutex).load(std::memory_order_acquire); + if (!isInitializedMutex(current)) + throw std::invalid_argument("pthread_mutex_setname_np: mutex is statically initialized or destroyed"); + if (current->_named) + return posixAgain; + if (name) { + current->_name = name; + current->_named = true; + } + return 0; +} + } diff --git a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp index 8d06b7e9ea..82d935c17c 100644 --- a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp @@ -1,3 +1,4 @@ +#include "prx/libkernel/include/Api.hpp" #include "prx/libkernel/Pthread/include/Rwlock.hpp" #include "../include/Pthread.hpp" #include "../include/Rwlock.hpp" @@ -17,6 +18,8 @@ static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = 0x8002003C; static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; static constexpr int FIRST_STORED_RWLOCK_TYPE = 1; static constexpr int LAST_STORED_RWLOCK_TYPE = 2; +static constexpr int POSIX_EINVAL = 22; +static constexpr int POSIX_EAGAIN = 35; static PthreadRwlockPrivate* const DestroyedRwlock = reinterpret_cast(std::uintptr_t{1}); @@ -188,4 +191,17 @@ int APS5_VABI scePthreadRwlockattrSettype(PthreadRwlockattr* attr, int type) { return SCE_OK; } +int APS5_VABI pthread_rwlock_setname_np_nid_postfix(PthreadRwlock* rwlock, const char* name) { + if (!rwlock) throw std::runtime_error("pthread_rwlock_setname_np: null rwlock"); + PthreadRwlock current = std::atomic_ref(*rwlock).load(std::memory_order_acquire); + if (current == nullptr) throw std::runtime_error("pthread_rwlock_setname_np: statically initialized rwlock"); + if (current == DestroyedRwlock) throw std::runtime_error("pthread_rwlock_setname_np: destroyed rwlock"); + std::lock_guard state(current->_state); + if (current->_named) return POSIX_EAGAIN; + if (!name) return POSIX_EINVAL; + current->_name = name; + current->_named = true; + return SCE_OK; +} + } diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 71680fca25..75a44a3cd2 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -488,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; @@ -955,7 +956,10 @@ extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name) { if (!thread || !entry) throw std::runtime_error("scePthreadCreate: null arg"); - if (name && std::strlen(name) >= GUEST_THREAD_NAME_CAPACITY) return SCE_KERNEL_ERROR_ENAMETOOLONG; + if (name && std::strlen(name) >= GUEST_THREAD_NAME_CAPACITY) { + *thread = nullptr; + return SCE_KERNEL_ERROR_ENAMETOOLONG; + } if (attr && !*attr) attr = nullptr; auto p = std::make_unique(attr ? *attr : nullptr); bool detached = false; diff --git a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp index a1b4e1bd60..96d8588896 100644 --- a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp +++ b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/Semaphore/include/Semaphore.hpp" #include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libc/include/General.hpp" #include #include #include @@ -21,12 +22,15 @@ extern "C" { int APS5_VABI sceKernelCreateSema(KernelSema* sem, const char* name, uint32_t attr, int init, int max, void* opt) { (void)opt; - if (sem == nullptr || attr > 2 || max < 0) { + if (sem == nullptr) { APS5_INVALID_ARG_EX; } - if (name == nullptr || init < 0 || max == 0 || init > max) { + if ((attr & ~0x103u) != 0 || (attr & 3u) == 3u || name == nullptr || init < 0 || max <= 0 || init > max) { return SCE_KERNEL_ERROR_EINVAL; } + if ((attr & 0x100u) != 0) { + NotImplemented_nid_no_patch("sceKernelCreateSema with attribute bit 0x100"); + } *sem = new KernelSemaPrivate(init, max, std::string(name), attr == 1); if (TraceSync()) std::fprintf(stderr, "[sema] create %p '%s' attr=0x%x init=%d max=%d\n", static_cast(*sem), name, attr, init, max); @@ -53,10 +57,7 @@ int APS5_VABI sceKernelSignalSema(KernelSema sem, int count) { if (sem == nullptr) { return SCE_KERNEL_ERROR_ESRCH; } - if (count < 0) { - APS5_INVALID_ARG_EX; - } - if (count == 0) { + if (count <= 0) { return SCE_KERNEL_ERROR_EINVAL; } diff --git a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp index 44990b4654..6a2ca1b693 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp @@ -40,3 +40,6 @@ int SetIpv6LevelOption(std::uintptr_t native, bool ipv6, int option, const void* int GetIpv6LevelOption(std::uintptr_t native, bool ipv6, int option, void* value, std::uint32_t* length); } + +extern "C" int GuestSocketSetIpv6LevelOption_nid_no_patch(std::uintptr_t native, bool ipv6, int option, const void* value, std::uint32_t length); +extern "C" int GuestSocketGetIpv6LevelOption_nid_no_patch(std::uintptr_t native, bool ipv6, int option, void* value, std::uint32_t* length); 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 7fb42adbcb..399e530a79 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -215,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()); diff --git a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp index 83b5231843..027d43d4c1 100644 --- a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp +++ b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp @@ -13,7 +13,9 @@ #endif #endif #include "prx/libkernel/Socket/include/SocketError.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/Socket/include/SocketOptions.hpp" +#include "prx/libc/include/General.hpp" #include #include #include @@ -28,6 +30,7 @@ constexpr int GuestDontRoute = 0x10; constexpr int GuestBroadcast = 0x20; constexpr int GuestLinger = 0x80; constexpr int GuestOobInline = 0x100; +constexpr int GuestAcceptFilter = 0x1000; constexpr int GuestNoSigpipe = 0x800; constexpr int GuestSendBuffer = 0x1001; constexpr int GuestReceiveBuffer = 0x1002; @@ -73,6 +76,7 @@ constexpr int ErrorNotSupported = 45; constexpr int ErrorAddressInUse = 48; constexpr int ErrorAddressNotAvailable = 49; +constexpr int ErrorFault = 14; constexpr int ErrorInvalid = 22; constexpr int ErrorDomain = 33; constexpr int ErrorNoProtocolOption = 42; @@ -156,6 +160,10 @@ int IpReceiveBit(int option) { int Copy(void* output, std::uint32_t* length, const void* value, std::uint32_t size) { const std::uint32_t count = output ? std::min(*length, size) : 0; + const GuestArena::HostWrite destination(count == 0 ? nullptr : output, count); + if (!destination.Open()) return ErrorFault; + const GuestArena::HostWrite lengthOutput(length, sizeof(*length)); + if (!lengthOutput.Open()) return ErrorFault; if (count != 0) std::memcpy(output, value, count); *length = count; return 0; @@ -165,6 +173,12 @@ int Copy(void* output, std::uint32_t* length, const void* value, std::uint32_t s int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int family, int type, int option, const void* value, std::uint32_t length) { const auto handle = static_cast(native); + if (option == GuestAcceptFilter) { + bool listening = false; + if (type != 2 && !NativeListening(native, listening)) return HostError(); + if (!listening) return ErrorInvalid; + NotImplemented_nid_no_patch("setsockopt: SO_ACCEPTFILTER on a listening socket"); + } if (option == GuestLinger) { if (length < sizeof(GuestLingerValue)) return ErrorInvalid; GuestLingerValue guest; @@ -274,6 +288,12 @@ bool NativeListening(std::uintptr_t native, bool& listening) { int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int family, int type, int option, void* value, std::uint32_t* length) { const auto handle = static_cast(native); + if (option == GuestAcceptFilter) { + bool listening = false; + if (type != 2 && !NativeListening(native, listening)) return HostError(); + if (!listening) return ErrorInvalid; + NotImplemented_nid_no_patch("getsockopt: SO_ACCEPTFILTER on a listening socket"); + } if (option == GuestLinger) { linger current{}; socklen_t size = sizeof(current); @@ -322,6 +342,12 @@ 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 && @@ -336,8 +362,8 @@ int SetTcpLevelOption(std::uintptr_t native, SocketOptionState& state, bool tcp, } 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) { @@ -512,3 +538,11 @@ int GetIpv6LevelOption(std::uintptr_t native, bool ipv6, int option, void* value } } + +extern "C" int GuestSocketSetIpv6LevelOption_nid_no_patch(std::uintptr_t native, bool ipv6, int option, const void* value, std::uint32_t length) { + return GuestSockets::SetIpv6LevelOption(native, ipv6, option, value, length); +} + +extern "C" int GuestSocketGetIpv6LevelOption_nid_no_patch(std::uintptr_t native, bool ipv6, int option, void* value, std::uint32_t* length) { + return GuestSockets::GetIpv6LevelOption(native, ipv6, option, value, length); +} diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index c84d53c5e9..1131f91a2a 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -42,6 +42,7 @@ #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/Time.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketConnect.hpp" @@ -124,6 +125,7 @@ struct Socket : KernelSocketPoll::NativeOwner { GuestSockets::SocketOptionState options; 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() { @@ -275,13 +277,12 @@ int AddressLengthError(const void* input, std::uint32_t length) { if (length < 2) return 22; return input ? 0 : 14; } -bool Address(int family, const void* input, std::uint32_t length, sockaddr_storage& native, socklen_t& size, bool lengthFirst = false) { +bool Address(int family, const void* input, std::uint32_t length, sockaddr_storage& native, socklen_t& size) { if (const int error = AddressLengthError(input, length)) { Fail(error); return false; } const auto* bytes = static_cast(input); 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 && bytes[1] != 2) { Fail(47); return false; } if (family == 2) { auto& v4 = reinterpret_cast(native); v4.sin_family = AF_INET; @@ -566,6 +567,16 @@ constexpr short NativePollPriority = POLLPRI; constexpr PollFlag RequestFlags[] = { {0x1, NativePollIn}, {GuestPollPriority, NativePollPriority}, {0x4, POLLOUT}, {0x40, POLLRDNORM}, {0x80, POLLRDBAND}, {0x100, POLLWRBAND}}; constexpr PollFlag StatusFlags[] = {{0x8, POLLERR}, {0x10, POLLHUP}, {GuestPollInvalid, POLLNVAL}}; +template +bool UrgentPending(Descriptor descriptor) { +#ifdef __APPLE__ + int atmark = 0; + return ::ioctl(static_cast(descriptor), SIOCATMARK, &atmark) == 0 && atmark != 0; +#else + static_cast(descriptor); + return false; +#endif +} std::shared_ptr Find(int descriptor) { std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); @@ -663,7 +674,7 @@ bool GuestSockets::PipeReady(int descriptor, short events, short& revents) { #endif namespace { -int DuplicateSocket_nid_no_patch(int descriptor, int minimum, bool closeOnExec) { +int DuplicateSocket_nid_no_patch(int descriptor, int minimum) { if (minimum < 0) return Fail(22); std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); @@ -672,8 +683,7 @@ int DuplicateSocket_nid_no_patch(int descriptor, int minimum, bool closeOnExec) if (duplicate < 0) return -1; try { sockets.insert_or_assign(duplicate, found->second); - if (closeOnExec) closeOnExecDescriptors.insert(duplicate); - else closeOnExecDescriptors.erase(duplicate); + closeOnExecDescriptors.erase(duplicate); } catch (const std::bad_alloc&) { sockets.erase(duplicate); ReleaseNumber(duplicate); @@ -684,7 +694,7 @@ int DuplicateSocket_nid_no_patch(int descriptor, int minimum, bool closeOnExec) } int GuestSockets::Duplicate(int descriptor) { - return DuplicateSocket_nid_no_patch(descriptor, 0, false); + return DuplicateSocket_nid_no_patch(descriptor, 0); } int GuestSockets::DuplicateTo(int descriptor, int target) { @@ -720,6 +730,8 @@ namespace { constexpr int GuestUnix = 1; constexpr int GuestCloseOnExec = 0x10000000; constexpr int GuestNonBlocking = 0x20000000; +constexpr std::uint32_t NoDevice = 0xFFFFFFFFu; +std::uint32_t lastUnixInode = 0; int SetNonBlocking(Socket& socket) { #ifdef _WIN32 unsigned long value = 1; @@ -879,11 +891,14 @@ int UnnamedAddress(void* address, std::uint32_t* length) { int GuestSockets::Stat(int descriptor, FileStat* status) { std::shared_ptr socket; + std::uint32_t inode = 0; { std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); if (found == sockets.end()) return 9; socket = found->second; + if (socket->family == GuestUnix && socket->inode == 0) socket->inode = ++lastUnixInode != 0 ? lastUnixInode : ++lastUnixInode; + inode = socket->inode; } bool receiveShut = false; bool sendShut = false; @@ -904,6 +919,10 @@ int GuestSockets::Stat(int descriptor, FileStat* status) { socklen_t length = sizeof(sendBuffer); if (getsockopt(socket->value, SOL_SOCKET, SO_SNDBUF, reinterpret_cast(&sendBuffer), &length) != 0) sendBuffer = 0; *status = FileStat{}; + if (socket->family == GuestUnix) { + status->st_dev = NoDevice; + status->st_ino = inode; + } status->st_mode = 0140000; if (!receiveShut || available > 0) status->st_mode |= 0444; if (!sendShut) status->st_mode |= 0222; @@ -1107,12 +1126,10 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { } return File::Fcntl(descriptor, command, argument); } - if (command == 0 || command == 17) return DuplicateSocket_nid_no_patch(descriptor, static_cast(argument), command == 17); - if (command == 10 || command == 18) { - const int duplicate = dup2_nid_postfix(descriptor, static_cast(argument)); - if (duplicate >= 0 && command == 18) fcntl_nid_postfix(duplicate, 2, 1); - return duplicate; - } + if (command == 0) return DuplicateSocket_nid_no_patch(descriptor, static_cast(argument)); + if (command == 17) return Fail(45); + if (command == 10) return dup2_nid_postfix(descriptor, static_cast(argument)); + if (command == 18) return DuplicateSocket_nid_no_patch(descriptor, 0); const auto socket = Lookup(descriptor); if (!socket) return -1; std::lock_guard lock(socket->modeMutex); @@ -1179,7 +1196,11 @@ int APS5_VABI getsockopt_nid_postfix(int descriptor, int level, int option, if (!socket) return -1; if (level != 0xffff && level != 0 && level != 6 && level != 41) return Fail(22); if (!length) return Fail(22); - if (!value) *length = 0; + if (!value) { + const GuestArena::HostWrite lengthOutput(length, sizeof(*length)); + if (!lengthOutput.Open()) return Fail(14); + *length = 0; + } std::lock_guard lock(socket->modeMutex); const auto native = static_cast(socket->value); const int error = level == 0 ? GuestSockets::GetIpLevelOption(socket->options, socket->family == 2, socket->type == 2, option, value, length) @@ -1217,6 +1238,7 @@ int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { 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) { @@ -1322,6 +1344,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); @@ -1502,7 +1525,7 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st } else { sockaddr_storage native{}; socklen_t size; - if (!Address(socket->family, address, addressLength, native, size, true)) return -1; + if (!Address(socket->family, address, addressLength, native, size)) 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)); @@ -1640,6 +1663,31 @@ static bool WriteGuest(void* destination, const void* source, std::size_t bytes) return true; #endif } +static std::vector ArrivalTimestamps(const Socket& socket, int keptFlags) { + std::vector records; + if (socket.type != 2 || (socket.family != 2 && socket.family != 28)) return records; + const bool binary = socket.family == 2 && (keptFlags & 0x2000) != 0; + const bool micro = (keptFlags & 0x400) != 0; + if (!binary && !micro) return records; + KernelTimespec now{}; + if (clock_gettime_nid_postfix(0, &now) != 0) throw std::runtime_error("recvmsg: reading CLOCK_REALTIME for the arrival time failed"); + const auto append = [&records](int type, std::int64_t seconds, std::uint64_t fraction) { + constexpr std::uint32_t length = 32; + constexpr int level = 0xffff; + const auto at = records.size(); + records.resize(at + length); + std::memcpy(records.data() + at, &length, sizeof(length)); + std::memcpy(records.data() + at + 4, &level, sizeof(level)); + std::memcpy(records.data() + at + 8, &type, sizeof(type)); + std::memcpy(records.data() + at + 16, &seconds, sizeof(seconds)); + std::memcpy(records.data() + at + 24, &fraction, sizeof(fraction)); + }; + const auto seconds = static_cast(now.tv_sec); + const auto nanoseconds = static_cast(now.tv_nsec); + if (binary) append(4, seconds, nanoseconds * 18446744073ull); + if (micro) append(2, seconds, nanoseconds / 1000); + return records; +} std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, int flags) { GuestMsghdr header{}; if (!message || !ReadGuest(&header, message, sizeof(header))) return Fail(14); @@ -1663,6 +1711,7 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, socklen_t size = 0; std::int64_t received = 0; const bool shut = ReceiveShut(*socket); + std::vector stamps; if (!shut && (total != 0 || socket->type == 1 || ReadableNow(*socket))) { size = sizeof(native); if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); @@ -1676,6 +1725,14 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, return Fail(NativeError()); #endif } + if (header.control) { + int keptFlags = 0; + { + std::lock_guard lock(socket->modeMutex); + keptFlags = socket->options.keptFlags; + } + stamps = ArrivalTimestamps(*socket, keptFlags); + } } header.flags = flags; if (received > total) { @@ -1701,7 +1758,11 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, if (!WriteGuest(header.name, address, header.nameLength)) return Fail(14); if (!WriteGuest(&message->nameLength, &header.nameLength, sizeof(header.nameLength))) return Fail(14); } - header.controlLength = 0; + const std::size_t controlCapacity = header.controlLength; + const std::size_t copied = std::min(stamps.size(), controlCapacity); + if (copied != 0 && !WriteGuest(header.control, stamps.data(), copied)) return Fail(14); + if (copied < stamps.size()) header.flags |= 0x20; + header.controlLength = static_cast(copied); constexpr auto results = offsetof(GuestMsghdr, controlLength); if (!WriteGuest(reinterpret_cast(message) + results, reinterpret_cast(&header) + results, sizeof(header) - results)) return Fail(14); return received; @@ -1818,13 +1879,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; } @@ -1868,11 +1929,15 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c #ifdef _WIN32 if (!pipe) native[i].revents = ClosedStreamEvents(entry.descriptor, native[i].revents, native[i].events); #endif + short revents = native[i].revents; + if (!pipe && (entry.events & GuestPollPriority) && (revents & NativePollPriority) == 0 && UrgentPending(native[i].fd)) { + revents = static_cast(revents | NativePollPriority | POLLRDBAND); + } for (const auto& flag : RequestFlags) - if ((entry.events & flag.guest) && (native[i].revents & flag.native)) + if ((entry.events & flag.guest) && (revents & flag.native)) entry.revents = static_cast(entry.revents | flag.guest); for (const auto& flag : StatusFlags) - if (native[i].revents & flag.native) entry.revents = static_cast(entry.revents | flag.guest); + if (revents & flag.native) entry.revents = static_cast(entry.revents | flag.guest); 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)); @@ -1882,13 +1947,31 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c if (entry.revents) ++ready; }; const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout < 0 ? 0 : timeout); +#ifdef __APPLE__ + bool waitsForUrgent = false; + for (std::size_t i = 0; i < socketCount && !waitsForUrgent; ++i) { + waitsForUrgent = (descriptors[owners[i]].events & GuestPollPriority) != 0; + } +#else + constexpr bool waitsForUrgent = false; +#endif #ifdef _WIN32 const bool sliced = !pipes.empty() || !kqueues.empty(); #else - const bool sliced = !kqueues.empty(); + const bool sliced = !kqueues.empty() || waitsForUrgent; #endif + const auto anyUrgentPending = [&]() -> bool { +#ifdef __APPLE__ + for (std::size_t i = 0; i < socketCount; ++i) { + const auto& entry = descriptors[owners[i]]; + if ((entry.events & GuestPollPriority) != 0 && (native[i].revents & NativePollPriority) == 0 && UrgentPending(native[i].fd)) return true; + } +#endif + return false; + }; for (;;) { int checkedReady = 0; + const bool urgent = anyUrgentPending(); #ifdef _WIN32 for (const auto index : pipes) { auto& entry = descriptors[index]; @@ -1908,7 +1991,7 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c if (entry.revents) ++checkedReady; } int slice = 0; - if (!ready && !checkedReady) { + if (!ready && !checkedReady && !urgent) { if (!sliced) slice = timeout; else if (timeout < 0) slice = 1; else { @@ -1923,7 +2006,7 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c #else if (::poll(native.data(), static_cast(native.size()), slice) < 0) return Fail(NativeError()); #endif - } else if (!ready && !checkedReady) { + } else if (!ready && !checkedReady && !urgent) { const auto wake = std::chrono::steady_clock::now() + std::chrono::milliseconds(1); PreciseSleepUntil(timeout < 0 ? wake : std::min(deadline, wake)); } @@ -1931,7 +2014,7 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c for (const auto& request : native) if (request.revents) ++socketReady; const bool expired = timeout >= 0 && std::chrono::steady_clock::now() >= deadline; - if (ready || checkedReady || socketReady || expired || !sliced) { + if (ready || checkedReady || urgent || socketReady || expired || !sliced) { ready += checkedReady; #ifdef _WIN32 for (std::size_t i = 0; i < native.size(); ++i) collect(i, false); diff --git a/core/libs/prx/libkernel/System/src/Login.cpp b/core/libs/prx/libkernel/System/src/Login.cpp index 82f71f218b..d25e16d5b2 100644 --- a/core/libs/prx/libkernel/System/src/Login.cpp +++ b/core/libs/prx/libkernel/System/src/Login.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -61,9 +62,32 @@ extern "C" void LoginNameForTest_nid_no_patch(const char* name) { 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 42a4926f4c..39cdaac297 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -17,6 +17,9 @@ #include #endif #endif +#if defined(__APPLE__) +#include +#endif #if defined(__linux__) && defined(__x86_64__) #include #include @@ -28,6 +31,7 @@ #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" @@ -45,11 +49,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -58,6 +64,7 @@ #include #endif +extern char** environ; namespace { constexpr int sceInvalidArgument = static_cast(0x80020016u); @@ -66,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; @@ -230,9 +246,6 @@ constexpr int guestRlimitFsize = 1; constexpr int guestPriorityProcess = 0; constexpr int guestPriorityGroup = 1; constexpr int guestPriorityUser = 2; -std::atomic guestNice{0}; -constexpr int guestNiceMin = -20; -constexpr int guestNiceMax = 20; constexpr int guestRlimitData = 2; constexpr int guestRlimitStack = 3; constexpr int guestRlimitCore = 4; @@ -283,6 +296,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 @@ -527,26 +815,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) { @@ -841,7 +1180,7 @@ int APS5_VABI getpriority_nid_postfix(int which, int who) { *__error_nid_postfix() = guestNoProcess; return -1; } - return guestNice.load(std::memory_order_relaxed); + return 0; } int APS5_VABI setpriority_nid_postfix(int which, int who, int niceval) { @@ -855,8 +1194,11 @@ int APS5_VABI setpriority_nid_postfix(int which, int who, int niceval) { *__error_nid_postfix() = guestNoProcess; return -1; } - const int clamped = niceval < guestNiceMin ? guestNiceMin : (niceval > guestNiceMax ? guestNiceMax : niceval); - guestNice.store(clamped, std::memory_order_relaxed); + if (which != guestPriorityProcess) { + throw std::runtime_error(std::string("setpriority: setting the nice value of ") + + (which == guestPriorityGroup ? "PRIO_PGRP" : "PRIO_USER") + " to " + std::to_string(niceval) + + " is not verified on the console"); + } 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 9ea06b2e2a..80cb2e88d3 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -5,6 +5,7 @@ #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 @@ -109,7 +110,7 @@ bool SignalBlocked(int guest) { struct PendingSignals { std::mutex lock; - std::condition_variable changed; + TimedWait::Condition changed; std::atomic words[4]{}; }; @@ -126,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) { @@ -298,26 +299,33 @@ bool DeliverPendingSignal(int guest) { return false; } -void DeliverUnblockedPending() { +bool DeliverUnblockedPending() { + bool caught = false; for (int guest = 1; guest <= MaxSignal; ++guest) { if (SignalBlocked(guest) || !Deliverable(guest) || !TakePending(guest)) continue; - DeliverPendingSignal(guest); + 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; } } @@ -336,7 +344,7 @@ void WaitForDeliverableSignal() { { std::unique_lock lock(pending.lock); if (!TakePendingUnblocked(&guest)) { - pending.changed.wait(lock); + pending.changed.Wait(lock); continue; } } @@ -540,7 +548,7 @@ thread_local GuestStack alternateStack{nullptr, 0, SsDisable}; namespace { -int ChangeBlockedMask(int how, const GuestSignalSet* set, GuestSignalSet* previousSet, bool releaseHolds) { +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); @@ -575,12 +583,18 @@ int ChangeBlockedMask(int how, const GuestSignalSet* set, GuestSignalSet* previo } } std::atomic_thread_fence(std::memory_order_seq_cst); - DeliverUnblockedPending(); + 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)); diff --git a/core/libs/prx/libkernel/System/src/SoftwareVersion.cpp b/core/libs/prx/libkernel/System/src/SoftwareVersion.cpp index 9c6bf6029c..7bba44ee84 100644 --- a/core/libs/prx/libkernel/System/src/SoftwareVersion.cpp +++ b/core/libs/prx/libkernel/System/src/SoftwareVersion.cpp @@ -21,8 +21,7 @@ constexpr std::uint32_t SdkPatchMask = 0xfff; extern "C" { int APS5_VABI sceKernelGetProsperoSystemSwVersion_nid_postfix(SceKernelProsperoSystemSwVersion* version) { - if (version == nullptr) return SCE_KERNEL_ERROR_EFAULT; - if (version->size != sizeof(SceKernelProsperoSystemSwVersion)) return SCE_KERNEL_ERROR_EINVAL; + if (version == nullptr) return 0; std::memset(version->versionString, 0, sizeof(version->versionString)); std::memcpy(version->versionString, SystemSoftwareVersionString, sizeof(SystemSoftwareVersionString)); version->version = SystemSoftwareVersion; diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index 7abef3192e..b6c40105c9 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]; @@ -135,6 +155,20 @@ struct GuestVmEntry { }; static_assert(offsetof(GuestVmEntry, start) == 8 && offsetof(GuestVmEntry, protection) == 56 && offsetof(GuestVmEntry, path) == 136 && sizeof(GuestVmEntry) == 144); +struct GuestKinfoProc { + std::int32_t structSize; + std::int32_t layout; + unsigned char reserved0[64]; + std::int32_t pid; + std::int32_t parentPid; + unsigned char reserved1[288]; + std::int64_t flag; + unsigned char reserved2[71]; + char command[20]; + unsigned char reserved3[621]; +}; +static_assert(offsetof(GuestKinfoProc, pid) == 72 && offsetof(GuestKinfoProc, parentPid) == 76 && offsetof(GuestKinfoProc, flag) == 368 && offsetof(GuestKinfoProc, command) == 447 && sizeof(GuestKinfoProc) == 1088); + std::vector GuestVmEntries() { std::vector entries; std::uintptr_t start = 0; @@ -161,9 +195,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", @@ -187,9 +225,19 @@ std::int64_t APS5_VABI sysconf_nid_postfix(int name) { case 1: // _SC_ARG_MAX result = 262144; break; + case 2: { + GuestResourceLimit limit{}; + if (getrlimit_nid_postfix(7, &limit) == 0) { + result = limit.rlim_cur; + } + break; + } case 3: // _SC_CLK_TCK result = 128; break; + case 4: + result = 1023; + break; case 5: { // _SC_OPEN_MAX GuestResourceLimit limit{}; if (getrlimit_nid_postfix(8, &limit) == 0) { @@ -197,6 +245,13 @@ std::int64_t APS5_VABI sysconf_nid_postfix(int name) { } break; } + case 26: { + GuestResourceLimit limit{}; + if (getrlimit_nid_postfix(8, &limit) == 0) { + result = std::min(limit.rlim_cur, 32767); + } + break; + } case 47: // _SC_PAGESIZE result = PS5_PAGE_SIZE; break; @@ -211,6 +266,12 @@ std::int64_t APS5_VABI sysconf_nid_postfix(int name) { result = ::sysconf(name == 57 ? _SC_NPROCESSORS_CONF : _SC_NPROCESSORS_ONLN); #endif break; + case 72: + result = 255; + break; + case 73: + result = 33; + break; case 121: { // _SC_PHYS_PAGES: report host physical capacity in guest pages #ifdef _WIN32 MEMORYSTATUSEX memory{}; @@ -270,15 +331,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; @@ -322,6 +374,26 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void static constexpr char path[] = "/app0/eboot.bin"; return CopyOut(path, sizeof(path), oldValue, oldLength); } + if (nameLength >= 3 && name[0] == 1 && name[1] == 14 && name[2] == 1) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + if (nameLength != 4) { + *__error_nid_postfix() = 22; + return -1; + } + if (name[3] != getpid_nid_postfix()) { + *__error_nid_postfix() = 3; + return -1; + } + GuestKinfoProc process{}; + process.structSize = sizeof(process); + process.pid = name[3]; + process.parentPid = getppid_nid_postfix(); + std::memcpy(process.command, "eboot.bin", sizeof("eboot.bin")); + return CopyOut(&process, sizeof(process), oldValue, oldLength); + } if (nameLength >= 3 && name[0] == 1 && name[1] == 14 && name[2] == 35) { if (newValue != nullptr) { *__error_nid_postfix() = 1; @@ -389,6 +461,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 +476,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; @@ -458,6 +545,38 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si const std::uint64_t frequency = sceKernelGetTscFrequency(); return CopyOut(&frequency, sizeof(frequency), oldValue, oldLength); } + if (std::strcmp(name, "machdep.upd_version") == 0) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + static constexpr std::uint32_t updVersion = 0; + return CopyOut(&updVersion, sizeof(updVersion), oldValue, oldLength); + } + if (std::strcmp(name, "machdep.curr_manumode") == 0) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + static constexpr int manumode = 0; + return CopyOut(&manumode, sizeof(manumode), oldValue, oldLength); + } + if (std::strcmp(name, "machdep.rcmgr_intdev") == 0) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + static constexpr int intdev = 0; + return CopyOut(&intdev, sizeof(intdev), oldValue, oldLength); + } + if (std::strcmp(name, "machdep.rcmgr_intdev_for_internal_libc") == 0) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + static constexpr int intdev = 0; + return CopyOut(&intdev, sizeof(intdev), oldValue, oldLength); + } if (std::strcmp(name, "hw.pagesize") == 0) { static constexpr int mib[] = {6, 7}; return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); @@ -466,10 +585,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); @@ -484,7 +599,7 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SceKernelSystemSwVersion* version) { if (version == nullptr) { - return SCE_KERNEL_ERROR_EFAULT; + return 0; } std::memset(version->versionString, 0, sizeof(version->versionString)); std::memcpy(version->versionString, "01.000.000", sizeof("01.000.000")); @@ -517,6 +632,43 @@ int APS5_VABI sysKernelGetLowerLimitUpdVersion_nid_postfix(std::uint32_t* versio return 0; } +int APS5_VABI sysKernelGetUpdVersion_nid_postfix(std::uint32_t* version) { + if (version == nullptr) { + return SCE_KERNEL_ERROR_EINVAL; + } + std::size_t length = sizeof(*version); + return sysctlbyname_nid_postfix("machdep.upd_version", version, &length, nullptr, 0); +} + +int APS5_VABI sysKernelGetManufacturingMode_nid_postfix(void) { + int mode = 0; + std::size_t length = sizeof(mode); + if (sysctlbyname_nid_postfix("machdep.curr_manumode", &mode, &length, nullptr, 0) != 0) { + return -1; + } + return mode; +} + +int APS5_VABI sysKernelGetIntdevModeForInternalLibc_nid_postfix(void) { + int mode = 0; + std::size_t length = sizeof(mode); + const int result = sysctlbyname_nid_postfix("machdep.rcmgr_intdev_for_internal_libc", &mode, &length, nullptr, 0); + if (result != 0) { + return result; + } + return mode; +} + +int APS5_VABI sysKernelGetIntdevModeForRcmgr_nid_postfix(void) { + int mode = 0; + std::size_t length = sizeof(mode); + const int result = sysctlbyname_nid_postfix("machdep.rcmgr_intdev", &mode, &length, nullptr, 0); + if (result != 0) { + return result; + } + return mode; +} + int APS5_VABI sysctlnametomib_nid_postfix(const char* name, int* mib, std::size_t* size) { struct KnownName { const char* name; @@ -541,13 +693,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 +711,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/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 60703eeb0e..852bb34a1c 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -770,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; } diff --git a/core/libs/prx/libkernel/include/Api.hpp b/core/libs/prx/libkernel/include/Api.hpp index a3d62e9c7b..cd2e4add21 100644 --- a/core/libs/prx/libkernel/include/Api.hpp +++ b/core/libs/prx/libkernel/include/Api.hpp @@ -10,6 +10,7 @@ #include #include #include +#include struct KernelIovec { void* base; @@ -116,6 +117,8 @@ struct PthreadBarrierPrivate { unsigned arrived = 0; unsigned active = 0; std::uint64_t generation = 0; + std::string name; + bool named = false; }; struct PthreadBarrierattrPrivate { @@ -215,6 +218,7 @@ int APS5_VABI sceKernelMapFlexibleMemory(void** addrInOut, size_t len, int prot, int APS5_VABI sceKernelMapNamedDirectMemory(void** addr, size_t len, int prot, int flags, int64_t directMemoryStart, size_t alignment, const char* name); int32_t APS5_VABI sceKernelMapNamedFlexibleMemory(void** addrInOut, size_t len, int prot, int flags, const char* name); int32_t APS5_VABI sceKernelMapNamedFlexibleMemoryInternal(void** addrInOut, size_t len, int prot, int flags, const char* name); +int32_t APS5_VABI sceKernelMapNamedSystemFlexibleMemory(void** addrInOut, size_t len, int prot, int flags, const char* name); int APS5_VABI sceKernelMprotect(const void* addr, size_t len, int prot); int APS5_VABI sceKernelMunmap(uint64_t vaddr, size_t len); int APS5_VABI sceKernelMmap(void* addr, size_t len, int prot, int flags, int fd, int64_t offset, void** res); @@ -414,6 +418,7 @@ int APS5_VABI pthread_barrierattr_setpshared_nid_postfix(PthreadBarrierattrPriva int APS5_VABI pthread_barrier_init_nid_postfix(PthreadBarrierPrivate** barrier, PthreadBarrierattrPrivate* const* attr, unsigned count); int APS5_VABI pthread_barrier_wait_nid_postfix(PthreadBarrierPrivate** barrier); int APS5_VABI pthread_barrier_destroy_nid_postfix(PthreadBarrierPrivate** barrier); +int APS5_VABI pthread_barrier_setname_np_nid_postfix(PthreadBarrierPrivate** barrier, const char* name); int APS5_VABI pthread_cond_destroy_nid_postfix(PthreadCond* cond); int APS5_VABI pthread_cond_broadcast_nid_postfix(PthreadCond* cond); int APS5_VABI pthread_cond_init_nid_postfix(PthreadCond* cond, const PthreadCondattr* attr); @@ -421,9 +426,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); @@ -438,6 +445,7 @@ int APS5_VABI pthread_mutex_getspinloops_np_nid_postfix(PthreadMutex* mutex, int int APS5_VABI pthread_mutex_setspinloops_np_nid_postfix(PthreadMutex* mutex, int count); int APS5_VABI pthread_mutex_getyieldloops_np_nid_postfix(PthreadMutex* mutex, int* count); int APS5_VABI pthread_mutex_setyieldloops_np_nid_postfix(PthreadMutex* mutex, int count); +int APS5_VABI pthread_mutex_setname_np_nid_postfix(PthreadMutex* mutex, const char* name); int APS5_VABI pthread_mutexattr_destroy_nid_postfix(PthreadMutexattr* attr); int APS5_VABI pthread_mutexattr_init_nid_postfix(PthreadMutexattr* attr); int APS5_VABI pthread_mutexattr_getpshared_nid_postfix(const PthreadMutexattr* attr, int* pshared); @@ -464,6 +472,7 @@ int APS5_VABI pthread_rwlock_tryrdlock_nid_postfix(PthreadRwlock* rwlock); int APS5_VABI pthread_rwlock_trywrlock_nid_postfix(PthreadRwlock* rwlock); int APS5_VABI pthread_rwlock_unlock_nid_postfix(PthreadRwlock* rwlock); int APS5_VABI pthread_rwlock_wrlock_nid_postfix(PthreadRwlock* rwlock); +int APS5_VABI pthread_rwlock_setname_np_nid_postfix(PthreadRwlock* rwlock, const char* name); int APS5_VABI sem_init_nid_postfix(void* sem, int pshared, unsigned int value); int APS5_VABI sem_destroy_nid_postfix(void* sem); int APS5_VABI sem_wait_nid_postfix(void* sem); @@ -647,6 +656,7 @@ 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); @@ -729,6 +739,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); @@ -788,12 +799,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); @@ -802,5 +816,9 @@ int APS5_VABI setpgrp_nid_postfix(int pid, int pgrp); int APS5_VABI sceKernelGetSystemExVersion(char* first, char* second); int APS5_VABI sysKernelGetLowerLimitSysexVersion_nid_postfix(char* version); int APS5_VABI sysKernelGetLowerLimitUpdVersion_nid_postfix(std::uint32_t* version); +int APS5_VABI sysKernelGetUpdVersion_nid_postfix(std::uint32_t* version); +int APS5_VABI sysKernelGetManufacturingMode_nid_postfix(void); +int APS5_VABI sysKernelGetIntdevModeForInternalLibc_nid_postfix(void); +int APS5_VABI sysKernelGetIntdevModeForRcmgr_nid_postfix(void); } diff --git a/core/libs/prx/libkernel/libkernel.hpp b/core/libs/prx/libkernel/libkernel.hpp index 3a32592846..fcecdc7aa2 100644 --- a/core/libs/prx/libkernel/libkernel.hpp +++ b/core/libs/prx/libkernel/libkernel.hpp @@ -11,4 +11,5 @@ #include "prx/libkernel/Pthread/include/Rwlock.hpp" #include "prx/libkernel/Pthread/include/ThreadLifecycle.hpp" #include "prx/libkernel/Semaphore/include/Semaphore.hpp" +#include "prx/libkernel/Socket/include/SocketOptions.hpp" #include "prx/libkernel/Time/include/Time.hpp" diff --git a/core/libs/prx/libkernel_sys/CMakeLists.txt b/core/libs/prx/libkernel_sys/CMakeLists.txt index 70631309ca..4464c335f4 100644 --- a/core/libs/prx/libkernel_sys/CMakeLists.txt +++ b/core/libs/prx/libkernel_sys/CMakeLists.txt @@ -1,3 +1,5 @@ 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}) + +target_sources(${LIB_NAME} PRIVATE Uuid.cpp) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 7ec076b09f..dffe12ca99 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -69,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; @@ -296,16 +291,6 @@ int APS5_VABI pthread_attr_setstackaddr_nid_postfix(void) { return 0; } -int APS5_VABI pthread_barrier_setname_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - 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; @@ -336,11 +321,6 @@ int APS5_VABI pthread_mutex_init_for_mono_nid_postfix(void) { return 0; } -int APS5_VABI pthread_mutex_setname_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI pthread_mutex_setprioceiling_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -372,16 +352,6 @@ int APS5_VABI pthread_resume_user_context_np_nid_postfix(void) { return 0; } -int APS5_VABI pthread_rwlock_setname_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - 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; @@ -979,11 +949,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; @@ -1405,21 +1370,11 @@ int APS5_VABI sceKernelIsAuthenticNeo(void) { return 1; } -int APS5_VABI sceKernelIsCEX(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelIsDebuggerAttached(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceKernelIsDevKit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelIsExperimentalBeta(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1508,10 +1463,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__); @@ -1543,11 +1494,6 @@ int APS5_VABI sceKernelLwfsWrite(void) { return 0; } -int APS5_VABI sceKernelMapNamedSystemFlexibleMemory(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelMapSanitizerShadowMemory(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -2171,10 +2117,6 @@ 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) { const int error = pthread_multi_np_nid_postfix(); @@ -2206,11 +2148,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; @@ -2221,11 +2158,6 @@ 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); @@ -2294,11 +2226,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 sigqueue_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -2324,26 +2251,6 @@ int APS5_VABI symlinkat_nid_postfix(void) { return 0; } -int APS5_VABI sysKernelGetIntdevModeForInternalLibc_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sysKernelGetIntdevModeForRcmgr_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sysKernelGetManufacturingMode_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sysKernelGetUpdVersion_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sysc_e00_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -2379,11 +2286,6 @@ int APS5_VABI utrace_nid_postfix(void) { return 0; } -int APS5_VABI uuidgen_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI vfork_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 4aa5f49a80..bdea27c837 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,20 +142,15 @@ 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); } } @@ -337,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); } @@ -351,6 +375,10 @@ 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)); } @@ -386,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) { @@ -401,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) { @@ -694,11 +722,7 @@ int APS5_VABI fchmodat_nid_postfix(int fd, const char* path, int mode, int flag) return 0; } } -#ifdef _WIN32 - if (::_wchmod(native.wstring().c_str(), mode & 07777) != 0) return static_cast(Fail(errno == ENOENT && HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestFileError(errno))); -#else - if (::chmod(native.c_str(), static_cast(mode & 07777)) != 0) return static_cast(Fail(errno == ENOENT && HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestFileError(errno))); -#endif + if (ChmodNative_nid_no_patch(native, mode & 07777) != 0) return static_cast(Fail(errno == ENOENT && HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestFileError(errno))); RecordWrittenPath_nid_no_patch(native); return 0; } @@ -725,15 +749,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; } @@ -797,6 +819,13 @@ int APS5_VABI unlinkat_nid_postfix(int fd, const char* path, int flag) { if (IsDotOrDotDot(path)) return static_cast(Fail(GuestEinval)); if (flag & GuestAtRemovedir) { if (std::filesystem::is_symlink(symlinkStatus)) return static_cast(Fail(GuestEnotdir)); +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0 && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { + std::string junctionTarget; + if (File::WindowsSymbolicLink::ReadJunction(native, junctionTarget, "unlinkat")) return static_cast(Fail(GuestEnotdir)); + } +#endif if (!std::filesystem::is_directory(symlinkStatus)) { if (HasNonDirectoryPrefix(native) || std::filesystem::exists(symlinkStatus)) return static_cast(Fail(GuestEnotdir)); @@ -826,6 +855,17 @@ int APS5_VABI unlinkat_nid_postfix(int fd, const char* path, int flag) { return static_cast(Fail(GuestEnotdir)); return static_cast(Fail(GuestEnoent)); } +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0 && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { + std::string junctionTarget; + if (File::WindowsSymbolicLink::ReadJunction(native, junctionTarget, "unlinkat")) { + if (RemoveDirectoryW(native.c_str()) == 0) return static_cast(Fail(GuestEperm)); + RecordWrittenPath_nid_no_patch(native); + return 0; + } + } +#endif if (std::filesystem::is_directory(symlinkStatus)) return static_cast(Fail(GuestEperm)); #ifdef _WIN32 struct _stat64 status{}; diff --git a/core/libs/prx/libkernel_sys/ThreadQueries.cpp b/core/libs/prx/libkernel_sys/ThreadQueries.cpp index 6bdc835871..397716777c 100644 --- a/core/libs/prx/libkernel_sys/ThreadQueries.cpp +++ b/core/libs/prx/libkernel_sys/ThreadQueries.cpp @@ -7,4 +7,8 @@ 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/Uuid.cpp b/core/libs/prx/libkernel_sys/Uuid.cpp new file mode 100644 index 0000000000..c3c0a0da72 --- /dev/null +++ b/core/libs/prx/libkernel_sys/Uuid.cpp @@ -0,0 +1,84 @@ +#include "prx/libkernel_sys/include/Api.hpp" +#include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/HostEntropy.hpp" + +static_assert(sizeof(GuestUuid) == 16); + +namespace { + +constexpr int GuestEfault = 14; +constexpr int GuestEinval = 22; +constexpr int GuestClockRealtime = 0; +constexpr int UuidgenMaxCount = 2048; +constexpr std::uint64_t UuidTimeMask = (std::uint64_t{1} << 60) - 1; +constexpr std::uint64_t UuidGregorianOffset = 0x01B21DD213814000ULL; + +std::mutex uuidMutex; +std::array uuidNode{}; +bool uuidNodeReady = false; +std::uint64_t uuidLastTime = 0; +std::uint16_t uuidLastSequence = 0; + +int Fail(int error) { + *__error_nid_postfix() = error; + return -1; +} + +std::uint64_t UuidTime() { + KernelTimespec now{}; + if (clock_gettime_nid_postfix(GuestClockRealtime, &now) != 0) throw std::runtime_error("uuidgen: clock_gettime(CLOCK_REALTIME) failed"); + const auto ticks = UuidGregorianOffset + static_cast(now.tv_sec) * 10000000ULL + static_cast(now.tv_nsec) / 100; + return ticks & UuidTimeMask; +} + +} + +extern "C" { + +int APS5_VABI uuidgen_nid_postfix(GuestUuid* store, int count) { + if (count < 1 || count > UuidgenMaxCount) return Fail(GuestEinval); + std::uint64_t time = 0; + std::uint16_t sequence = 0; + std::array node{}; + { + std::lock_guard lock(uuidMutex); + if (!uuidNodeReady) { + HostEntropy_nid_no_patch(uuidNode.data(), uuidNode.size()); + uuidNode[0] |= 0x01; + uuidNodeReady = true; + } + node = uuidNode; + time = UuidTime(); + if (uuidLastTime == 0) { + HostEntropy_nid_no_patch(&sequence, sizeof(sequence)); + sequence &= 0x3fff; + } else if (uuidLastTime >= time) { + sequence = static_cast((uuidLastSequence + 1) & 0x3fff); + } else { + sequence = uuidLastSequence; + } + uuidLastSequence = sequence; + uuidLastTime = (time + static_cast(count) - 1) & UuidTimeMask; + } + if (store == nullptr) return Fail(GuestEfault); + const auto variantSequence = static_cast(sequence | 0x8000); + for (int index = 0; index < count; ++index, ++time) { + store[index].timeLow = static_cast(time); + store[index].timeMid = static_cast(time >> 32); + store[index].timeHiAndVersion = static_cast(((time >> 48) & 0x0fff) | 0x1000); + store[index].clockSeqHiAndReserved = static_cast(variantSequence >> 8); + store[index].clockSeqLow = static_cast(variantSequence); + std::memcpy(store[index].node, node.data(), node.size()); + } + return 0; +} + +} diff --git a/core/libs/prx/libkernel_sys/include/Api.hpp b/core/libs/prx/libkernel_sys/include/Api.hpp index 53c0802c35..f0f810c38b 100644 --- a/core/libs/prx/libkernel_sys/include/Api.hpp +++ b/core/libs/prx/libkernel_sys/include/Api.hpp @@ -24,6 +24,23 @@ 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); + +struct GuestUuid { + std::uint32_t timeLow; + std::uint16_t timeMid; + std::uint16_t timeHiAndVersion; + std::uint8_t clockSeqHiAndReserved; + std::uint8_t clockSeqLow; + std::uint8_t node[6]; +}; + extern "C" { int APS5_VABI aio_read_nid_postfix(GuestAiocb* aiocb); @@ -41,6 +58,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); @@ -81,7 +99,7 @@ int APS5_VABI cpuset_getid_nid_postfix(void); int APS5_VABI cpuset_setid_nid_postfix(void); int APS5_VABI dynlib_get_obj_member_nid_postfix(void); int APS5_VABI futimesat_nid_postfix(void); -int APS5_VABI get_authinfo_nid_postfix(void); +int APS5_VABI get_authinfo_nid_postfix(int pid, KernelAuthInfo* info); int APS5_VABI get_module_info_list_nid_postfix(void); int APS5_VABI get_page_table_stats_nid_postfix(void); int APS5_VABI get_sdk_compiled_version_nid_postfix(void); @@ -126,8 +144,6 @@ 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(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); @@ -135,15 +151,12 @@ int APS5_VABI pthread_main_np_nid_postfix(void); int APS5_VABI pthread_multi_np_nid_postfix(void); int APS5_VABI pthread_mutex_getprioceiling_nid_postfix(void); int APS5_VABI pthread_mutex_init_for_mono_nid_postfix(void); -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); 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 +278,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,14 +385,12 @@ 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); int APS5_VABI sceKernelLwfsSetAttribute(void); int APS5_VABI sceKernelLwfsTrimBlock(void); int APS5_VABI sceKernelLwfsWrite(void); -int APS5_VABI sceKernelMapNamedSystemFlexibleMemory(void); int APS5_VABI sceKernelMapSanitizerShadowMemory(void); int APS5_VABI sceKernelMapToolMemory(void); int APS5_VABI sceKernelMapTraceMemory(void); @@ -515,10 +526,8 @@ 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(Pthread thread); void APS5_VABI scePthreadResumeAll(void); int APS5_VABI scePthreadSetBesteffort(void); @@ -533,16 +542,12 @@ 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 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 swapon_nid_postfix(void); int APS5_VABI symlinkat_nid_postfix(void); -int APS5_VABI sysKernelGetIntdevModeForInternalLibc_nid_postfix(void); -int APS5_VABI sysKernelGetIntdevModeForRcmgr_nid_postfix(void); -int APS5_VABI sysKernelGetManufacturingMode_nid_postfix(void); -int APS5_VABI sysKernelGetUpdVersion_nid_postfix(void); int APS5_VABI sysc_e00_nid_postfix(void); int APS5_VABI sysc_m00_nid_postfix(void); int APS5_VABI sysc_s00_nid_postfix(void); @@ -550,7 +555,7 @@ int APS5_VABI tcgetsid_nid_postfix(void); int APS5_VABI tcsetsid_nid_postfix(void); int APS5_VABI unmount_nid_postfix(void); int APS5_VABI utrace_nid_postfix(void); -int APS5_VABI uuidgen_nid_postfix(void); +int APS5_VABI uuidgen_nid_postfix(GuestUuid* store, int count); int APS5_VABI vfork_nid_postfix(void); int APS5_VABI wait3_nid_postfix(void); int APS5_VABI wait4_nid_postfix(void); 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/Formatting.cpp b/core/libs/tests/Formatting.cpp index ad74c98f20..6b06e1d1dc 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -540,4 +540,26 @@ static void CheckDecimalFloat() { } } -int main() { CheckSizeLimits(); RunChecks(); CheckHexFloat(); CheckDecimalFloat(); CheckAllocatingFormat(); return CheckWidePrecision() ? 0 : 1; } +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/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 32522e33fd..7b8c328e40 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -1445,7 +1445,7 @@ void TestM4aacInitialize(std::uint32_t context) { Require(Refused(context, info)); Require(sceAjmInstanceDestroy(context, instance) == 0); - Require(sceAjmInstanceCreate(context, CODEC_M4AAC, 3ull << 3, &instance) == 0); + Require(sceAjmInstanceCreate(context, CODEC_M4AAC, 7ull << 3, &instance) == 0); const AacInitialize raw{2, 3}; std::int32_t rawResult[2] = {-1, -1}; Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); @@ -1556,6 +1556,48 @@ ResampleInfo GetResampleInfo(std::uint32_t context, std::uint32_t instance) { return resample; } +void TestBuildersWithoutBuffers(std::uint32_t context) { + const auto instance = CreateOpus(context); + const std::uint32_t parameters[3] = {2, 48000, 0}; + const GaplessDecode gapless{2000, 100, 0}; + std::int64_t results[5] = {-1, -1, -1, -1, -1}; + ResampleInfo resample; + std::memset(&resample, 0xAA, sizeof(resample)); + std::vector batch(4096); + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + std::size_t offset = 0; + const auto built = [&](int status) { + const bool grew = status == 0 && info.offset > offset; + offset = info.offset; + return grew; + }; + Require(built(sceAjmBatchJobInitialize(&info, instance, parameters, sizeof(parameters), &results[0]))); + Require(built(sceAjmBatchJobClearContext(&info, instance, &results[1]))); + Require(built(sceAjmBatchJobSetGaplessDecode(&info, instance, &gapless, 1, &results[2]))); + Require(built(sceAjmBatchJobSetResampleParameters(&info, instance, 1.0f, 1, &results[3]))); + Require(built(sceAjmBatchJobSetResampleParametersEx(&info, instance, 1.0f, 0.0f, 1, &results[4]))); + Require(built(sceAjmBatchJobGetResampleInfo(&info, instance, &resample))); + Submit(context, info); + Require(std::all_of(std::begin(results), std::end(results), [](std::int64_t result) { return result == 0; })); + Require(resample.result == 0 && resample.internalResult == 0 && resample.ratio == 1.0f && resample.samples == 0); + + const auto nullListThrows = [&](std::size_t inputs, std::size_t outputs) { + const auto before = info.offset; + try { + sceAjmBatchJobRunSplit(&info, instance, 0, nullptr, inputs, nullptr, outputs, nullptr, 0); + } catch (const std::runtime_error&) { + return info.offset == before; + } + return false; + }; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(nullListThrows(1, 0) && nullListThrows(0, 1) && nullListThrows(2, 3)); + offset = 0; + Require(built(sceAjmBatchJobRunSplit(&info, instance, 0, nullptr, 0, nullptr, 0, nullptr, 0))); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + struct ResampledStream { std::vector pcm; std::size_t jobs = 0; @@ -2396,6 +2438,7 @@ int main() { TestResampleOpus(context); TestResampleMp3(context); TestResampleParametersEx(context); + TestBuildersWithoutBuffers(context); TestResampleAt9(context); TestAt9GaplessSegments(context); TestAt9RunDecodesWholeInput(context); @@ -2407,5 +2450,6 @@ int main() { TestConsoleEmptyBatch(context); TestConsoleSingleFrameRun(context); TestConsoleStaleHeaderBits(context); + Require(sceAjmDecWVorbisCreateHeaderPacket(nullptr) == static_cast(0x8A6E0001)); Require(sceAjmFinalize(context) == 0); } diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index d39befaa2b..e1197f4a94 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -29,6 +29,9 @@ int main() { 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); 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/GuestAudio3dParameters.cpp b/core/libs/tests/GuestAudio3dParameters.cpp index 8382df155a..232c2e1065 100644 --- a/core/libs/tests/GuestAudio3dParameters.cpp +++ b/core/libs/tests/GuestAudio3dParameters.cpp @@ -1,11 +1,14 @@ #include "prx/libSceAudio3d/include/Audio3d.hpp" #include "SceTypes.hpp" #include +#include +#include #include #include #include #include + static_assert(sizeof(Audio3dOpenParameters) == 0x28); static_assert(offsetof(Audio3dOpenParameters, num_beds) == 0x20); @@ -69,7 +72,29 @@ static void CheckExtendedBuffer() { } } +static void CheckErrorStrings() { + constexpr const char* messages[]{ + "port does not exist", "object does not exist", "invalid parameter", + "out of memory", "out of resources", "not ready" + }; + for (std::uint32_t index = 0; index < std::size(messages); ++index) { + errno = 7; + const char* result = sceAudio3dStrError(std::bit_cast(0x80ea0002u + index)); + Require(result && std::strcmp(result, messages[index]) == 0, "incorrect Audio3d error description"); + Require(errno == 7, "error description lookup must preserve errno"); + Require(std::strcmp(sceAudio3dStrError(0), "(unknown)") == 0 && std::strcmp(result, messages[index]) == 0, + "error descriptions must remain valid after another lookup"); + } + for (const auto code : std::array{0, 1, 0xffffffffu, 0x80000000u, 0x7fffffffu, + 0x80e9ffffu, 0x80ea0000u, 0x80ea0001u, 0x80ea0008u, 0x80ea0009u, 0x80eb0002u}) { + errno = 7; + const char* result = sceAudio3dStrError(std::bit_cast(code)); + Require(result && std::strcmp(result, "(unknown)") == 0 && errno == 7, + "unrecognized codes must return the native fallback without changing errno"); + } +} int main() { + CheckErrorStrings(); CheckInvalidDestination(); CheckShortBuffer(); CheckExtendedBuffer(); 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/GuestCamera.cpp b/core/libs/tests/GuestCamera.cpp index c074795e23..06c5b3b7ee 100644 --- a/core/libs/tests/GuestCamera.cpp +++ b/core/libs/tests/GuestCamera.cpp @@ -1,5 +1,4 @@ -#include "prx/libSceCamera/include/Api.hpp" -#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceCamera/libSceCamera.hpp" #include #include #include @@ -9,6 +8,10 @@ namespace { constexpr int ErrorParam = static_cast(0x802E0000u); +constexpr int ErrorNotOpen = static_cast(0x802E0004u); +constexpr int ErrorFormatUnknown = static_cast(0x802E0007u); +constexpr int ErrorNotConnected = static_cast(0x802E0010u); +constexpr std::int32_t SystemUser = 0xFF; void Require(bool condition, const char* message) { if (!condition) { @@ -28,21 +31,112 @@ void RequireNotImplemented(TCall call, const std::string& name) { Require(false, "a declared camera function returned"); } +CameraStartParameter Start(std::uint32_t level0, std::uint32_t level1) { + return {sizeof(CameraStartParameter), {level0, level1}, nullptr}; +} + +void CheckClosed() { + const CameraStartParameter start = Start(1, 1); + CameraFrameData frame{}; + frame.sizeThis = sizeof(CameraFrameData); + Require(sceCameraClose(1) == ErrorNotOpen, "closing before any open must report NOT_OPEN"); + Require(sceCameraCloseByHandle(1) == ErrorNotOpen, "closing by handle before any open must report NOT_OPEN"); + Require(sceCameraStart(1, &start) == ErrorNotOpen, "starting before any open must report NOT_OPEN"); + Require(sceCameraStartByHandle(1, &start) == ErrorNotOpen, "starting by handle before any open must report NOT_OPEN"); + Require(sceCameraStop(1) == ErrorNotOpen, "stopping before any open must report NOT_OPEN"); + Require(sceCameraStopByHandle(1) == ErrorNotOpen, "stopping by handle before any open must report NOT_OPEN"); + Require(sceCameraIsValidFrameData(1, &frame) == ErrorNotOpen, "frame validity before any open must report NOT_OPEN"); +} + +void CheckOpen() { + Require(sceCameraOpen(1, 0, 0, nullptr) == ErrorParam, "a user other than the system user must be rejected"); + Require(sceCameraOpen(SystemUser, 1, 0, nullptr) == ErrorParam, "a camera type other than 0 must be rejected"); + Require(sceCameraOpen(SystemUser, 0, 1, nullptr) == ErrorParam, "a camera index other than 0 must be rejected"); + Require(sceCameraOpen(SystemUser, 0, 0, nullptr) == 1, "the first open must return handle 1"); + Require(sceCameraOpen(SystemUser, 0, 0, nullptr) == 2, "the second open must return handle 2"); +} + +void CheckStart() { + CameraStartParameter start = Start(1, 1); + Require(sceCameraStart(0, &start) == ErrorParam, "handle 0 must be rejected"); + Require(sceCameraStart(1, nullptr) == ErrorParam, "a null start parameter must be rejected"); + start.sizeThis = 16; + Require(sceCameraStart(1, &start) == ErrorParam, "a start parameter of the wrong size must be rejected"); + start = Start(0, 0); + Require(sceCameraStart(1, &start) == ErrorFormatUnknown, "two empty format levels must be rejected"); + start = Start(0x10, 1); + Require(sceCameraStart(1, &start) == ErrorFormatUnknown, "a format level above 0xF must be rejected"); + start = Start(1, 0); + Require(sceCameraStart(1, &start) == ErrorNotConnected, "starting without a device must report NOT_CONNECTED"); + Require(sceCameraStartByHandle(0, &start) == ErrorParam, "starting by handle 0 must be rejected"); + Require(sceCameraStartByHandle(1, &start) == 0, "starting by handle must succeed while a handle is open"); + Require(sceCameraStop(0) == ErrorParam, "stopping handle 0 must be rejected"); + Require(sceCameraStop(1) == 0, "stopping an open handle must succeed"); + Require(sceCameraStopByHandle(1) == 0, "stopping by handle must succeed"); +} + +void CheckFrames() { + CameraFrameData frame{}; + frame.sizeThis = sizeof(CameraFrameData); + frame.status[0] = 1; + frame.status[1] = 1; + Require(sceCameraIsValidFrameData(1, &frame) == 1, "frame data with both statuses at 1 must be valid"); + Require(sceCameraGetFrameData(1, nullptr) == ErrorParam, "a null frame must be rejected"); + Require(sceCameraGetFrameData(1, &frame) == ErrorNotOpen, "no frame may be read without a device"); + Require(frame.status[0] == UINT32_MAX && frame.status[1] == UINT32_MAX, "a failed read must mark both statuses -1"); + Require(sceCameraIsValidFrameData(1, &frame) == 0, "frame data from a failed read must be invalid"); + frame.status[0] = 1; + Require(sceCameraIsValidFrameData(1, &frame) == 0, "frame data with only the first status at 1 must be invalid"); + frame.sizeThis = sizeof(CameraFrameData) + 1; + Require(sceCameraGetFrameData(1, &frame) == ErrorParam, "an oversized frame must be rejected"); + Require(frame.status[0] == UINT32_MAX, "an oversized frame must still have its statuses reset"); + Require(sceCameraIsValidFrameData(1, &frame) == ErrorParam, "validating an oversized frame must be rejected"); + frame.sizeThis = sizeof(CameraFrameData); + Require(sceCameraGetFrameData(0, &frame) == ErrorParam, "reading handle 0 must be rejected"); + Require(sceCameraIsValidFrameData(0, &frame) == ErrorParam, "validating handle 0 must be rejected"); + Require(sceCameraIsValidFrameData(1, nullptr) == ErrorParam, "validating a null frame must be rejected"); +} + +void CheckClose() { + Require(sceCameraClose(0) == ErrorParam, "closing handle 0 must be rejected"); + const CameraStartParameter start = Start(1, 1); + CameraFrameData frame{}; + frame.sizeThis = sizeof(frame); + frame.status[0] = frame.status[1] = 1; + for (const int handle : {99, 1}) { + if (handle == 1) Require(sceCameraClose(1) == 0, "closing the first handle must succeed"); + Require(sceCameraClose(handle) == ErrorNotOpen, "a stale or fabricated handle must not close another session"); + Require(sceCameraCloseByHandle(handle) == ErrorNotOpen, "close by handle must reject a stale or fabricated handle"); + Require(sceCameraStart(handle, &start) == ErrorNotOpen, "start must reject a stale or fabricated handle"); + Require(sceCameraStartByHandle(handle, &start) == ErrorNotOpen, "start by handle must reject a stale or fabricated handle"); + Require(sceCameraStop(handle) == ErrorNotOpen, "stop must reject a stale or fabricated handle"); + Require(sceCameraStopByHandle(handle) == ErrorNotOpen, "stop by handle must reject a stale or fabricated handle"); + Require(sceCameraIsValidFrameData(handle, &frame) == ErrorNotOpen, "frame validation must reject a stale or fabricated handle"); + Require(sceCameraStop(2) == 0, "the second session must remain open"); + } + const int third = sceCameraOpen(SystemUser, 0, 0, nullptr); + Require(third > 2, "opening after a close must not reuse a live or stale handle"); + Require(sceCameraCloseByHandle(2) == 0, "closing the second handle must succeed"); + Require(sceCameraStop(third) == 0, "closing the second handle must preserve the third"); + Require(sceCameraClose(third) == 0, "closing the third handle must succeed"); + const int reopened = sceCameraOpen(SystemUser, 0, 0, nullptr); + Require(reopened > third, "reopening must not revive a stale handle"); + Require(sceCameraClose(third) == ErrorNotOpen, "a stale handle must not close the reopened session"); + Require(sceCameraClose(reopened) == 0, "closing the reopened handle must succeed"); +} + } int main() { Require(sceCameraIsAttached(0) == 0, "camera 0 must be reported as not attached"); Require(sceCameraIsAttached(1) == ErrorParam, "camera index 1 must be rejected"); Require(sceCameraIsAttached(-1) == ErrorParam, "a negative camera index must be rejected"); - RequireNotImplemented([] { sceCameraOpen(); }, "sceCameraOpen"); - RequireNotImplemented([] { sceCameraClose(); }, "sceCameraClose"); - RequireNotImplemented([] { sceCameraStart(); }, "sceCameraStart"); - RequireNotImplemented([] { sceCameraStop(); }, "sceCameraStop"); - RequireNotImplemented([] { sceCameraGetConfig(); }, "sceCameraGetConfig"); - RequireNotImplemented([] { sceCameraSetConfig(); }, "sceCameraSetConfig"); - RequireNotImplemented([] { sceCameraSetAttribute(); }, "sceCameraSetAttribute"); - RequireNotImplemented([] { sceCameraGetDeviceInfo(); }, "sceCameraGetDeviceInfo"); - RequireNotImplemented([] { sceCameraGetFrameData(); }, "sceCameraGetFrameData"); - RequireNotImplemented([] { sceCameraIsValidFrameData(); }, "sceCameraIsValidFrameData"); + CheckClosed(); + CheckOpen(); + CheckStart(); + CheckFrames(); + CheckClose(); + RequireNotImplemented([] { sceCameraSetVideoSync(); }, "sceCameraSetVideoSync"); + RequireNotImplemented([] { sceCameraGetCalibData(); }, "sceCameraGetCalibData"); return 0; } diff --git a/core/libs/tests/GuestCameraSettings.cpp b/core/libs/tests/GuestCameraSettings.cpp new file mode 100644 index 0000000000..8628cdbe19 --- /dev/null +++ b/core/libs/tests/GuestCameraSettings.cpp @@ -0,0 +1,213 @@ +#include "prx/libSceCamera/libSceCamera.hpp" +#include +#include +#include +#include +#include + +using GetU32 = int(APS5_VABI*)(std::int32_t, std::uint32_t, std::uint32_t*, void*); +using SetU32 = int(APS5_VABI*)(std::int32_t, std::uint32_t, std::uint32_t, const void*); + +namespace { + +constexpr int ErrorParam = static_cast(0x802E0000u); +constexpr int ErrorNotInit = static_cast(0x802E0002u); +constexpr int ErrorNotOpen = static_cast(0x802E0004u); +constexpr int ErrorNotConnected = static_cast(0x802E0010u); +constexpr int ErrorFatal = static_cast(0x802E00FFu); +constexpr std::uint32_t Channel0 = 1; +constexpr std::uint32_t Channel1 = 2; +constexpr std::uint32_t ChannelBoth = 3; + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "Camera settings: %s\n", message); + std::abort(); + } +} + +CameraConfig SizedConfig() { + CameraConfig config; + std::memset(&config, 0x5a, sizeof(config)); + config.sizeThis = sizeof(CameraConfig); + return config; +} + +void CheckBeforeOpen() { + std::uint32_t value = 0; + CameraConfig config = SizedConfig(); + const CameraDeviceInfo info{sizeof(CameraDeviceInfo), 1, 0, 0}; + Require(sceCameraGetContrast(1, Channel0, &value, nullptr) == ErrorNotOpen, "a getter before open must report NOT_OPEN"); + Require(sceCameraSetContrast(1, Channel0, 1, nullptr) == ErrorNotOpen, "a setter before open must report NOT_OPEN"); + Require(sceCameraGetConfig(1, &config) == ErrorNotOpen, "reading the configuration before open must report NOT_OPEN"); + Require(sceCameraGetDeviceConfigWithoutHandle(&config) == ErrorNotOpen, "reading the device configuration before open must report NOT_OPEN"); + Require(sceCameraGetMmapConnectedCount(&value) == ErrorNotOpen, "the mapped device count before open must report NOT_OPEN"); + Require(sceCameraGetDeviceInfo(0, &info) == ErrorNotInit, "device info before open must report NOT_INIT"); + Require(sceCameraGetProductInfo(&value) == ErrorNotInit, "product info before open must report NOT_INIT"); + Require(sceCameraGetRegistryInfo(&value) == ErrorNotInit, "registry info before open must report NOT_INIT"); +} + +void CheckGetter(GetU32 get, std::uint32_t expected, const char* message) { + std::uint32_t value = 0x5a5a5a5a; + Require(get(1, Channel1, &value, nullptr) == 0 && value == expected, message); + value = 0x5a5a5a5a; + std::uint32_t option = 0; + Require(get(0, Channel0, &value, nullptr) == ErrorParam, "a getter must reject handle 0"); + Require(get(99, Channel0, &value, nullptr) == ErrorNotOpen && value == 0x5a5a5a5a, "a fabricated handle must not read settings"); + Require(get(1, 0, &value, nullptr) == ErrorParam, "a getter must reject channel 0"); + Require(get(1, ChannelBoth, &value, nullptr) == ErrorParam, "a getter must reject both channels"); + Require(get(1, Channel0, nullptr, nullptr) == ErrorParam, "a getter must reject a null result"); + Require(get(1, Channel0, &value, &option) == ErrorParam, "a getter must reject an option"); + Require(value == 0x5a5a5a5a, "a refused getter must not write its result"); +} + +void CheckGetters() { + CheckGetter(sceCameraGetAutoWhiteBalance, 0, "automatic white balance must be off"); + CheckGetter(sceCameraGetDefectivePixelCancellation, 0, "defective pixel cancellation must be off"); + CheckGetter(sceCameraGetLensCorrection, 0, "lens correction must be off"); + CheckGetter(sceCameraGetContrast, 32, "the contrast must be 32"); + CheckGetter(sceCameraGetSaturation, 64, "the saturation must be 64"); + CheckGetter(sceCameraGetSharpness, 1, "the sharpness must be 1"); + std::int32_t hue = 0; + Require(sceCameraGetHue(1, Channel0, &hue, nullptr) == 0 && hue == 1, "the hue must be 1"); + CameraExposureGain exposure{9, 9, 9, 9}; + Require(sceCameraGetExposureGain(1, Channel0, &exposure, nullptr) == 0, "the exposure must be readable"); + Require(exposure.exposureControl == 0 && exposure.exposure == 20 && exposure.gain == 100 && exposure.mode == 0, "the exposure defaults are wrong"); + CameraWhiteBalance balance{9, 9, 9, 9}; + Require(sceCameraGetWhiteBalance(1, Channel0, &balance, nullptr) == 0, "the white balance must be readable"); + Require(balance.whiteBalanceControl == 0 && balance.gainRed == 768 && balance.gainBlue == 768 && balance.gainGreen == 512, "the white balance defaults are wrong"); + CameraGamma gamma; + std::memset(&gamma, 0x5a, sizeof(gamma)); + Require(sceCameraGetGamma(1, Channel0, &gamma, nullptr) == 0 && gamma.gammaControl == 0 && gamma.value == 4 && gamma.reserved[15] == 0, "the gamma defaults are wrong"); + std::uint32_t enable = 9; + CameraAutoExposureGainTarget target{sizeof(CameraAutoExposureGainTarget), 0x10}; + Require(sceCameraGetAutoExposureGain(1, Channel0, &enable, &target) == 0 && enable == 0, "automatic exposure must be off"); + Require(target.sizeThis == 0 && target.target == 0, "the automatic exposure target must be cleared"); + target.sizeThis = 4; + Require(sceCameraGetAutoExposureGain(1, Channel0, &enable, &target) == ErrorParam, "a target of the wrong size must be rejected"); + Require(sceCameraGetAutoExposureGain(1, Channel0, &enable, nullptr) == 0, "automatic exposure must be readable without a target"); +} + +void CheckAttribute() { + CameraAttribute attribute; + std::memset(&attribute, 0x5a, sizeof(attribute)); + attribute.sizeThis = sizeof(CameraAttribute); + attribute.channel = Channel1; + Require(sceCameraGetAttribute(1, &attribute) == 0, "the attributes must be readable"); + Require(attribute.sizeThis == sizeof(CameraAttribute) && attribute.channel == Channel1, "the attribute header must be kept"); + Require(attribute.framePosition.x == 0 && attribute.exposureGain.exposure == 83 && attribute.exposureGain.gain == 100, "the attribute exposure is wrong"); + Require(attribute.whiteBalance.gainRed == 768 && attribute.whiteBalance.gainGreen == 512 && attribute.gamma.value == 4, "the attribute colour defaults are wrong"); + Require(attribute.saturation == 64 && attribute.contrast == 32 && attribute.sharpness == 1 && attribute.hue == 1 && attribute.reserved[3] == 0, "the attribute levels are wrong"); + attribute.channel = ChannelBoth; + Require(sceCameraGetAttribute(1, &attribute) == ErrorParam, "reading both channels at once must be rejected"); + Require(sceCameraSetAttribute(1, &attribute) == ErrorNotConnected, "writing both channels must report NOT_CONNECTED"); + attribute.channel = 4; + Require(sceCameraSetAttribute(1, &attribute) == ErrorParam, "an unknown channel must be rejected"); + attribute.channel = Channel0; + attribute.sizeThis = sizeof(CameraAttribute) - 4; + Require(sceCameraGetAttribute(1, &attribute) == ErrorParam && sceCameraSetAttribute(1, &attribute) == ErrorParam, "an attribute of the wrong size must be rejected"); + Require(sceCameraGetAttribute(1, nullptr) == ErrorParam && sceCameraSetAttribute(1, nullptr) == ErrorParam, "a null attribute must be rejected"); +} + +void CheckSetter(SetU32 set, std::uint32_t largestValue) { + std::uint32_t option = 0; + Require(set(1, ChannelBoth, largestValue, nullptr) == ErrorNotConnected, "a setter must report NOT_CONNECTED"); + Require(set(0, Channel0, 0, nullptr) == ErrorParam, "a setter must reject handle 0"); + Require(set(1, 4, 0, nullptr) == ErrorParam, "a setter must reject channel 4"); + Require(set(1, Channel0, 0, &option) == ErrorParam, "a setter must reject an option"); + if (largestValue != UINT32_MAX) Require(set(1, Channel0, largestValue + 1, nullptr) == ErrorParam, "a setter must reject a value past its range"); +} + +void CheckSetters() { + CheckSetter(sceCameraSetAutoWhiteBalance, UINT32_MAX); + CheckSetter(sceCameraSetContrast, UINT32_MAX); + CheckSetter(sceCameraSetSaturation, UINT32_MAX); + CheckSetter(sceCameraSetDefectivePixelCancellation, 1); + CheckSetter(sceCameraSetDefectivePixelCancellationInternal, 2); + CheckSetter(sceCameraSetLensCorrection, 1); + CheckSetter(sceCameraSetLensCorrectionInternal, 2); + CheckSetter(sceCameraSetSharpness, 10); + Require(sceCameraSetHue(1, Channel0, -5, nullptr) == ErrorNotConnected, "a hue change must report NOT_CONNECTED"); + const CameraExposureGain exposure{}; + const CameraWhiteBalance balance{}; + const CameraGamma gamma{}; + Require(sceCameraSetExposureGain(1, Channel0, &exposure, nullptr) == ErrorNotConnected, "an exposure change must report NOT_CONNECTED"); + Require(sceCameraSetWhiteBalance(1, Channel0, &balance, nullptr) == ErrorNotConnected, "a white balance change must report NOT_CONNECTED"); + Require(sceCameraSetGamma(1, Channel0, &gamma, nullptr) == ErrorNotConnected, "a gamma change must report NOT_CONNECTED"); + Require(sceCameraSetExposureGain(1, Channel0, nullptr, nullptr) == ErrorParam, "a null exposure must be rejected"); + Require(sceCameraSetWhiteBalance(1, Channel0, nullptr, nullptr) == ErrorParam, "a null white balance must be rejected"); + Require(sceCameraSetGamma(1, Channel0, nullptr, nullptr) == ErrorParam, "a null gamma must be rejected"); + CameraAutoExposureGainTarget target{sizeof(CameraAutoExposureGainTarget), 0x20}; + Require(sceCameraSetAutoExposureGain(1, Channel0, 1, &target) == ErrorNotConnected, "the largest exposure target must be accepted"); + Require(sceCameraSetAutoExposureGain(1, Channel0, 1, nullptr) == ErrorNotConnected, "automatic exposure without a target must be accepted"); + for (const std::uint32_t accepted : {0x0u, 0x2u, 0x4u, 0x6u, 0x8u, 0x10u, 0x12u, 0x14u, 0x16u, 0x20u}) { + target.target = accepted; + Require(sceCameraSetAutoExposureGain(1, Channel0, 1, &target) == ErrorNotConnected, "a listed exposure target must be accepted"); + } + for (const std::uint32_t refused : {0x1u, 0xAu, 0xCu, 0xEu, 0x11u, 0x18u, 0x1Au, 0x1Cu, 0x1Eu, 0x22u}) { + target.target = refused; + Require(sceCameraSetAutoExposureGain(1, Channel0, 1, &target) == ErrorParam, "an unlisted exposure target must be rejected"); + } + target = {4, 0x10}; + Require(sceCameraSetAutoExposureGain(1, Channel0, 1, &target) == ErrorParam, "an exposure target of the wrong size must be rejected"); +} + +void CheckConfig() { + CameraConfig config = SizedConfig(); + Require(sceCameraGetConfig(1, &config) == 0 && config.configType == 1, "the configuration must be type 1"); + for (const CameraConfigExtension& extension : config.extension) { + Require(extension.formatLevel[0] == 0 && extension.formatLevel[1] == 4 && extension.formatLevel[3] == 4, "the default formats are wrong"); + Require(extension.resolution == 0 && extension.framerate == 60 && extension.width == 0 && extension.baseOption == nullptr, "the default mode is wrong"); + } + Require(sceCameraSetConfig(1, &config) == ErrorNotConnected, "a new configuration must report NOT_CONNECTED"); + Require(sceCameraSetConfigInternal(1, &config) == ErrorNotConnected, "an internal configuration must report NOT_CONNECTED"); + config = SizedConfig(); + Require(sceCameraGetDeviceConfig(1, &config) == 0 && config.sizeThis == sizeof(CameraConfig) && config.configType == 0, "the device configuration must be empty"); + Require(config.extension[1].baseOption == nullptr, "the device configuration must be cleared"); + config = SizedConfig(); + Require(sceCameraGetDeviceConfigWithoutHandle(&config) == 0 && config.sizeThis == 0 && config.configType == 0, "the handleless device configuration must be cleared"); + config = SizedConfig(); + config.sizeThis = 96; + Require(sceCameraGetConfig(1, &config) == ErrorParam && sceCameraSetConfig(1, &config) == ErrorParam, "a configuration of the wrong size must be rejected"); + Require(sceCameraGetDeviceConfig(1, &config) == ErrorParam && sceCameraGetDeviceConfigWithoutHandle(&config) == ErrorParam, "a device configuration of the wrong size must be rejected"); + Require(sceCameraGetConfig(0, nullptr) == ErrorParam && sceCameraSetConfigInternal(1, nullptr) == ErrorParam, "a missing configuration must be rejected"); +} + +void CheckDevice() { + CameraDeviceInfo info{sizeof(CameraDeviceInfo), 1, 0, 0}; + std::array out{}; + Require(sceCameraGetDeviceInfo(0, &info) == ErrorNotConnected, "device info must report NOT_CONNECTED"); + Require(sceCameraGetDeviceInfo(1, &info) == ErrorParam, "a reserved value other than 0 must be rejected"); + info.infoRevision = 2; + Require(sceCameraGetDeviceInfo(0, &info) == ErrorParam, "an unknown info revision must be rejected"); + Require(sceCameraGetDeviceInfo(0, nullptr) == ErrorParam, "null device info must be rejected"); + Require(sceCameraGetProductInfo(out.data()) == ErrorNotConnected && sceCameraGetRegistryInfo(out.data()) == ErrorNotConnected, "product and registry info must report NOT_CONNECTED"); + Require(sceCameraGetProductInfo(nullptr) == ErrorParam && sceCameraGetRegistryInfo(nullptr) == ErrorParam, "null product or registry info must be rejected"); + std::uint32_t count = 9; + Require(sceCameraGetMmapConnectedCount(&count) == 0 && count == 0, "no camera may be mapped"); + Require(sceCameraGetMmapConnectedCount(nullptr) == ErrorParam, "a null count must be rejected"); + Require(sceCameraGetCalibDataFromDevice() == ErrorNotConnected, "device calibration must report NOT_CONNECTED"); + CameraGetCalibrationDataParameter param{sizeof(CameraGetCalibrationDataParameter), 0, 1}; + Require(sceCameraGetCalibrationData(¶m, out.data()) == ErrorNotConnected, "calibration data must report NOT_CONNECTED"); + Require(sceCameraGetCalibrationData(¶m, nullptr) == ErrorParam && sceCameraGetCalibrationData(nullptr, out.data()) == ErrorParam, "missing calibration arguments must be rejected"); + for (const CameraGetCalibrationDataParameter refused : {CameraGetCalibrationDataParameter{8, 0, 0}, CameraGetCalibrationDataParameter{12, 1, 0}, CameraGetCalibrationDataParameter{12, 0, 2}}) { + Require(sceCameraGetCalibrationData(&refused, out.data()) == ErrorParam, "a bad calibration request must be rejected"); + } + Require(sceCameraAudioOpen() == ErrorFatal, "camera audio must fail with FATAL"); +} + +} + +int main() { + CheckBeforeOpen(); + Require(sceCameraOpen(0xFF, 0, 0, nullptr) == 1, "the camera must open"); + CheckGetters(); + CheckAttribute(); + CheckSetters(); + CheckConfig(); + CheckDevice(); + Require(sceCameraClose(1) == 0, "the camera must close"); + std::uint32_t value = 0; + Require(sceCameraGetSharpness(1, Channel0, &value, nullptr) == ErrorNotOpen, "a getter after close must report NOT_OPEN"); + return 0; +} diff --git a/core/libs/tests/GuestCes.cpp b/core/libs/tests/GuestCes.cpp index b978d9f935..bb5862cb5e 100644 --- a/core/libs/tests/GuestCes.cpp +++ b/core/libs/tests/GuestCes.cpp @@ -361,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) { @@ -498,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); diff --git a/core/libs/tests/GuestCesChinese.cpp b/core/libs/tests/GuestCesChinese.cpp index fa1490c296..424a2a27bd 100644 --- a/core/libs/tests/GuestCesChinese.cpp +++ b/core/libs/tests/GuestCesChinese.cpp @@ -82,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); @@ -98,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) { @@ -164,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; diff --git a/core/libs/tests/GuestCesDbcs.cpp b/core/libs/tests/GuestCesDbcs.cpp index a4bf0db643..bd3edaa068 100644 --- a/core/libs/tests/GuestCesDbcs.cpp +++ b/core/libs/tests/GuestCesDbcs.cpp @@ -86,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; } @@ -184,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)); } } } @@ -321,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); @@ -365,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); diff --git a/core/libs/tests/GuestCesJapanese.cpp b/core/libs/tests/GuestCesJapanese.cpp index 30bfa8b235..42b8e2c16e 100644 --- a/core/libs/tests/GuestCesJapanese.cpp +++ b/core/libs/tests/GuestCesJapanese.cpp @@ -18,22 +18,11 @@ 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; @@ -69,12 +58,12 @@ static bool EncodeLeavesOutputs(int expected, const void* profile, std::uint16_t return result == expected && size == (expected == static_cast(0x805C0001) ? 77u : 0u) && mbcs[0] == 0xEE && mbcs[1] == 0xEE && mbcs[2] == 0xEE; } -static bool DecodeFails(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); if (!sjis) return result == expected && ucs2 == 0; - return result == expected && used == 77 && ucs2 == 0xEEEE; + return result == expected && used == length && ucs2 == 0; } static void SweepEucJisX0208(const void* profile) { @@ -86,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; @@ -101,7 +88,7 @@ static void SweepEucJisX0208(const void* profile) { hash = Digest(hash, value); } } - Require(assigned == jisX0208Cells && hash == jisX0208Digest); + Require(assigned == jisX0208Cells && hash == eucJisX0208Digest); } static void SweepSJisJisX0208(const void* profile) { @@ -115,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; @@ -125,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) { @@ -159,12 +144,15 @@ 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(DecodeFails(unassignedCode, profile, "\xAD\xA1", 2, false) && DecodeFails(unassignedCode, profile, "\xF9\xA1", 2, false)); - Require(DecodeFails(unassignedCode, profile, "\xA9\xA1", 2, false) && DecodeFails(unassignedCode, profile, "\xFE\xFE", 2, false)); - Require(DecodeFails(invalidEncode, profile, "\xA4\x41", 2, false) && DecodeFails(invalidEncode, profile, "\xA4\xFF", 2, false)); - Require(DecodeFails(invalidEncode, profile, "\x80", 1, false) && DecodeFails(invalidEncode, profile, "\xFF", 1, false)); - Require(DecodeFails(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 == 2 && mbcs[0] == 0xEE); @@ -174,8 +162,8 @@ static void TestJisX0208Profiles(const void* cp932, const void* cp51932) { Require(EucEncodes(cp51932, 0x7E8A, "\xF9\xA1") && EucEncodes(ss3, 0x7E8A, "\x8F\xD4\xE3")); Require(EncodeLeavesOutputs(unassignedCode, euc, 0x7E8A, false) && EncodeLeavesOutputs(unassignedCode, ss2, 0x7E8A, false)); - Require(DecodeFails(invalidEncode, euc, "\x8E\xB1", 2, false) && DecodeFails(invalidEncode, euc, "\x8E", 1, false)); - Require(DecodeFails(invalidEncode, euc, "\x8F\xB0\xA1", 3, false) && DecodeFails(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}) { @@ -183,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(DecodeFails(invalidEncode, profile, "\x8E\xE0", 2, false) && DecodeFails(invalidEncode, profile, "\x8E\xA0", 2, false)); - Require(DecodeFails(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; @@ -203,7 +191,7 @@ static void TestJisX0208Profiles(const void* cp932, const void* cp51932) { } } Require(supplementary == jisX0212Cells && hash == jisX0212Digest); - Require(DecodeFails(srcBufferEnd, ss3, "\x8F\xB0", 2, false) && DecodeFails(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] = {}; @@ -221,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(DecodeFails(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(DecodeFails(invalidEncode, sjis, "\x82\x7F", 2, true) && DecodeFails(invalidEncode, sjis, "\x82\xFD", 2, true)); - Require(DecodeFails(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(DecodeFails(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() { diff --git a/core/libs/tests/GuestCesMbc.cpp b/core/libs/tests/GuestCesMbc.cpp index e88d2f94d6..9d1e17c780 100644 --- a/core/libs/tests/GuestCesMbc.cpp +++ b/core/libs/tests/GuestCesMbc.cpp @@ -196,6 +196,37 @@ static void RequireInvalid(const void* profile) { } } +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; @@ -228,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); @@ -315,12 +362,8 @@ int main() { alignas(8) std::uint8_t eucCnSheet[64] = {}; alignas(8) std::uint8_t eucKrSheet[64] = {}; - alignas(8) std::uint8_t uhcSheet[64] = {}; - alignas(8) std::uint8_t big5Sheet[64] = {}; const void* eucCn = sceCesUcsProfileInitEucCnGb2312(eucCnSheet); const void* eucKr = sceCesUcsProfileInitEucKr(eucKrSheet); - const void* uhc = sceCesUcsProfileInitUhc(uhcSheet); - const void* big5 = sceCesUcsProfileInitBig5Cp950(big5Sheet); const void* cp1251 = sceCesRefersUcsProfileCp1251(); const void* cp1250 = sceCesRefersUcsProfileCp1250(); 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 c316de1c76..bac0286fe0 100644 --- a/core/libs/tests/GuestContentSearch.cpp +++ b/core/libs/tests/GuestContentSearch.cpp @@ -36,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() { @@ -55,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); @@ -125,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/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 0ff64c7371..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"); @@ -182,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"); @@ -206,6 +409,7 @@ void TestLongDoubleHelpers() { int main() { TestStod(); TestStoll(); + TestDoubleHelpers(); TestFloatErrors(); TestIntegerErrors(); TestArithmetic(); 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/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..8a081181a5 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{}; @@ -257,5 +264,42 @@ int main() { Require(futimes_nid_postfix(pipe[0], first) == -1 && *__error_nid_postfix() == 22, "pipe timestamp update fails"); #endif Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0, "close pipe"); + const auto throughFile = (root / "file.txt" / "child").string(); + for (const auto update : {utimes_nid_postfix, lutimes_nid_postfix}) { + Require(update(throughFile.c_str(), first) == -1 && *__error_nid_postfix() == 20, "a path through a file reports ENOTDIR"); + } +#ifdef _WIN32 + const auto fileLink = (root / "file-link.txt").string(); + const auto directoryLink = (root / "directory-link").string(); + const auto danglingLink = (root / "dangling-link").string(); + const auto createLink = [](const std::string& link, const std::string& target, DWORD flags) { + wchar_t full[MAX_PATH]; + GetFullPathNameW(std::filesystem::path(target).c_str(), MAX_PATH, full, nullptr); + return CreateSymbolicLinkW(std::filesystem::path(link).c_str(), full, flags | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0; + }; + if (createLink(fileLink, path, 0) && createLink(directoryLink, directory, SYMBOLIC_LINK_FLAG_DIRECTORY) && + createLink(danglingLink, (root / "missing").string(), 0)) { + const auto modified = [](const char* name, bool link) { + FileStat status{}; + Require((link ? lstat_nid_postfix(name, &status) : stat_nid_postfix(name, &status)) == 0, "stat a link fixture"); + return status.st_mtim.tv_sec; + }; + Require(utimes_nid_postfix(fileLink.c_str(), first) == 0 && modified(path.c_str(), false) == first[1].tv_sec, "utimes follows a file link"); + Require(modified(fileLink.c_str(), true) != first[1].tv_sec, "utimes leaves the file link alone"); + Require(utimes_nid_postfix(directoryLink.c_str(), second) == 0 && modified(directory.c_str(), false) == second[1].tv_sec, + "utimes follows a directory link"); + Require(utimes_nid_postfix(danglingLink.c_str(), first) == -1 && *__error_nid_postfix() == 2, "utimes through a dangling link reports ENOENT"); + Require(lutimes_nid_postfix(directoryLink.c_str(), first) == 0 && modified(directoryLink.c_str(), true) == first[1].tv_sec && + modified(directory.c_str(), false) == second[1].tv_sec, "lutimes changes a directory link and not its target"); + Require(lutimes_nid_postfix(danglingLink.c_str(), first) == 0 && modified(danglingLink.c_str(), true) == first[1].tv_sec, + "lutimes changes a dangling link"); + const auto throughLink = fileLink + "/child"; + for (const auto update : {utimes_nid_postfix, lutimes_nid_postfix}) { + Require(update(throughLink.c_str(), first) == -1 && *__error_nid_postfix() == 20, "a path through a file link reports ENOTDIR"); + } + Require(RemoveDirectoryW(std::filesystem::path(directoryLink).c_str()) && DeleteFileW(std::filesystem::path(fileLink).c_str()) && + DeleteFileW(std::filesystem::path(danglingLink).c_str()), "remove the link fixtures"); + } +#endif std::filesystem::remove_all(root); } 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 9eb5a65154..a412640fac 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -634,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); @@ -1098,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); @@ -1161,6 +1194,22 @@ int main() { nullptr, 0, &returned, nullptr) != 0); Require(CloseHandle(taggedCleanup) != 0 && std::filesystem::remove(tagged)); } + + const auto fullTarget = root / "junction-full-target"; + const auto fullJunction = root / "junction-full"; + Require(std::filesystem::create_directory(fullTarget)); + FILE* const inside = std::fopen((fullTarget / "inside.txt").string().c_str(), "wb"); + Require(inside != nullptr); + std::fclose(inside); + const auto fullCommand = L"cmd /c mklink /J \"" + fullJunction.wstring() + L"\" \"" + fullTarget.wstring() + L"\""; + Require(_wsystem(fullCommand.c_str()) == 0); + Require(rmdir_nid_postfix(fullJunction.string().c_str()) == -1 && *__error_nid_postfix() == 20); + Require(sceKernelRmdir(fullJunction.string().c_str()) == static_cast(0x80020014u)); + Require(std::filesystem::is_regular_file(fullTarget / "inside.txt")); + Require(sceKernelUnlink(fullJunction.string().c_str()) == 0); + Require(!std::filesystem::exists(std::filesystem::symlink_status(fullJunction))); + Require(std::filesystem::is_regular_file(fullTarget / "inside.txt")); + Require(std::filesystem::remove(fullTarget / "inside.txt") && std::filesystem::remove(fullTarget)); #endif FileStat identity{}; FileStat byDescriptor{}; @@ -1294,6 +1343,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/GuestFloatingPoint.cpp b/core/libs/tests/GuestFloatingPoint.cpp index 6d9afd5ae6..5d312a1460 100644 --- a/core/libs/tests/GuestFloatingPoint.cpp +++ b/core/libs/tests/GuestFloatingPoint.cpp @@ -66,13 +66,14 @@ void CheckEnabledExceptions() { state.x87[1] = (state.x87[1] & ~0x3fu) | (x87 & 0x15u); state.sse = (state.sse & ~0x7fbfu) | 0x2000 | (sse << 7) | (sse & 0x2au); WriteEnvironment(state); + const auto written = ReadEnvironment(); *__error_nid_postfix() = 123; const int enabled = fegetexcept_nid_postfix(); const int error = *__error_nid_postfix(); const auto after = ReadEnvironment(); WriteEnvironment(seeded); Require(enabled == static_cast(0x3f ^ x87), "enabled exceptions use the x87 masks independently of SSE"); - Require(after.x87[0] == state.x87[0] && after.x87[1] == state.x87[1] && after.sse == state.sse, + Require(after.x87[0] == written.x87[0] && after.x87[1] == written.x87[1] && after.sse == written.sse, "enabled exception query preserves control modes and flags"); Require(error == 123, "enabled exception query preserves errno"); } @@ -138,6 +139,8 @@ void CheckFlags() { Require(fetestexcept_nid_postfix(0) == 0, "empty exception query"); Require(fetestexcept_nid_postfix(0x3f) == 0x27, "combine x87 and SSE flags, including denormals"); Require(fetestexcept_nid_postfix(0x0c) == 0x04, "query only requested flags"); + const auto unmasked = ReadEnvironment(); + Require(fetestexcept_nid_postfix(0xffff) == static_cast((unmasked.x87[1] | unmasked.sse) & 0xffffu), "a mask past the exception flags returns the other status and MXCSR bits"); struct { std::uint16_t before, value, after; } saved{0x1234, 0xffff, 0x5678}; Require(fegetexceptflag_nid_postfix(&saved.value, 0x07) == 0 && saved.value == 0x07, "save selected flags"); Require(saved.before == 0x1234 && saved.after == 0x5678, "fexcept_t writes exactly two bytes"); diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index ef5ea31984..093c88fe45 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -671,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/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/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index 86df877d10..ed1987ef28 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -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 @@ -1257,7 +1259,232 @@ 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; 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/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/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..163760ae30 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(sceHttp2CookieFlush(box) == 0); + 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/GuestHttp2Cookies.cpp b/core/libs/tests/GuestHttp2Cookies.cpp new file mode 100644 index 0000000000..7fe8e67263 --- /dev/null +++ b/core/libs/tests/GuestHttp2Cookies.cpp @@ -0,0 +1,195 @@ +#include "prx/libSceHttp2/include/Http2.hpp" +#include +#include +#include +#include +#include + +static void Require(bool value) { if (!value) std::abort(); } + +static constexpr int NotFound = static_cast(0x817B1025); +static constexpr int InvalidId = static_cast(0x817B1100); +static constexpr int OutOfSize = static_cast(0x817B1104); +static constexpr int InvalidValue = static_cast(0x817B11FE); +static constexpr int PoolTooSmall = static_cast(0x817B121D); +static constexpr int Rejected = static_cast(0x817B1219); + +static int Add(int box, const char* url, const std::string& cookie) { + return sceHttp2AddCookie(box, url, cookie.c_str(), cookie.size()); +} + +static std::string Get(int box, const char* url) { + std::size_t required = 0; + Require(sceHttp2GetCookie(box, url, nullptr, &required, 0) == 0 && required >= 3); + std::string header(required, '\x7f'); + std::size_t again = 0; + Require(sceHttp2GetCookie(box, url, header.data(), &again, required) == 0 && again == required); + Require(header.compare(required - 3, 3, std::string("\0 \0", 3)) == 0); + header.resize(required - 3); + return header; +} + +static bool NoMatch(int box, const char* url) { + std::size_t required = 7; + char buffer[16] = {'x'}; + return sceHttp2GetCookie(box, url, nullptr, &required, 0) == NotFound && required == 7 && + sceHttp2GetCookie(box, url, buffer, &required, sizeof(buffer)) == NotFound && required == 7 && buffer[0] == 'x'; +} + +static Http2CookieStats Stats(int box) { + alignas(8) unsigned char raw[40]; + std::memset(raw, 0xff, sizeof(raw)); + Require(sceHttp2GetCookieStats(box, reinterpret_cast(raw)) == 0); + for (std::size_t i = 12; i < 16; ++i) Require(raw[i] == 0xff); + for (std::size_t i = 32; i < 40; ++i) Require(raw[i] == 0xff); + Http2CookieStats stats{}; + std::memcpy(&stats, raw, sizeof(stats)); + return stats; +} + +static int FillDomain(int box, int count) { + for (int i = 0; i < count; ++i) { + if (Add(box, "http://example.com/", "c" + std::to_string(i) + "=1") != 0) return -1; + } + return 0; +} + +int main() { + static_assert(sizeof(Http2CookieStats) == 32); + static_assert(offsetof(Http2CookieStats, currentInuseNum) == 8 && offsetof(Http2CookieStats, maxInuseSize) == 16); + static_assert(offsetof(Http2CookieStats, maxInuseNum) == 24 && offsetof(Http2CookieStats, removedNum) == 28); + constexpr std::size_t maxSize = static_cast(-1); + Require(sceHttp2CreateCookieBox(maxSize) < 0); + Require(sceHttp2CreateCookieBox(maxSize - 0x3fff) < 0); + Require(sceHttp2CreateCookieBox(maxSize / 2) < 0); + Require(sceHttp2CreateCookieBox(0xffff) == PoolTooSmall); + const int box = sceHttp2CreateCookieBox(0x10000); + Require(box > 0); + + Require(NoMatch(box, "http://example.com/")); + Require(Add(box, "http://example.com/a/b", "sid=1") == 0); + Require(Add(box, "http://Example.COM:8080/", "lang=fr; Path=/; Domain=.example.com") == 0); + Require(Add(box, "https://example.com/", "token=x; Secure; HttpOnly; Path=/") == 0); + Require(Add(box, "http://example.com/", "wide=1; Path=/a; Max-Age=3600") == 0); + Require(Get(box, "http://example.com/a/c") == "sid=1; wide=1; lang=fr"); + Require(Get(box, "https://example.com/a/b?q#f") == "sid=1; wide=1; token=x; lang=fr"); + Require(Get(box, "HTTPS://example.com/") == "token=x; lang=fr"); + Require(Get(box, "http://example.com/") == "lang=fr"); + Require(Get(box, "http://example.com/a") == "wide=1; lang=fr"); + Require(Get(box, "http://example.com/ab") == "wide=1; lang=fr"); + Require(Get(box, "http://www.example.com/a/") == "sid=1; wide=1; lang=fr"); + Require(Get(box, "ftp://example.com/a/b") == "sid=1; wide=1; lang=fr"); + Require(Get(box, "example.com/a/b") == "sid=1; wide=1; lang=fr"); + Require(Get(box, "http://user:pw@example.com/a/x") == "sid=1; wide=1; lang=fr"); + Require(NoMatch(box, "http://badexample.com/a")); + Require(NoMatch(box, "http:///a")); + Require(NoMatch(box, "")); + + Require(Add(box, "http://example.com/a/b", "sid=2") == 0); + Require(Get(box, "http://example.com/a/") == "sid=2; wide=1; lang=fr"); + Require(Add(box, "http://example.com/", "wide=0; Path=/a; Expires=Thu, 01 Jan 1970 00:00:00 GMT") == 0); + Require(Add(box, "http://example.com/", "lang=; Domain=example.com; Max-Age=0") == 0); + Require(Get(box, "http://example.com/a/") == "sid=2"); + Require(Add(box, "http://example.com/", "future=1; Expires=Wed, 09 Jun 2121 10:18:14 GMT") == 0); + Require(Add(box, "http://example.com/", "invalid=1; Expires=Feb 30 2121 10:18:14") == 0); + Require(Get(box, "http://example.com/") == "invalid=1; future=1"); + + Require(Add(box, "http://example.com/", "noequals") == 0); + Require(Add(box, "http://example.com/", "=value") == 0); + Require(Add(box, "http://example.com/", "evil=1; Domain=other.com") == Rejected); + Require(Add(box, "http://example.com/", "evil=1; Domain=ample.com") == Rejected); + Require(NoMatch(box, "http://other.com/")); + Require(Get(box, "http://example.com/") == "invalid=1; noequals; value; future=1"); + Require(sceHttp2AddCookie(box, "http://example.com/", "cut=12345", 5) == 0); + Require(Get(box, "http://example.com/") == "invalid=1; noequals; value; cut=1; future=1"); + + Http2CookieStats stats = Stats(box); + Require(stats.currentInuseNum == 7 && stats.currentInuseSize == 23 + 23 + 24 + 25 + 25 + 22 + 21); + Require(stats.maxInuseNum == 7 && stats.maxInuseSize == stats.currentInuseSize && stats.removedNum == 0); + + const std::string header = "invalid=1; noequals; value; cut=1; future=1"; + std::size_t required = 0; + char small[8] = {'x', 'x'}; + Require(sceHttp2GetCookie(box, "http://example.com/", small, &required, sizeof(small)) == OutOfSize && required == 0 && small[0] == '\0' && small[1] == 'x'); + std::string large(64, 'x'); + Require(sceHttp2GetCookie(box, "http://example.com/", large.data(), nullptr, large.size()) == 0 && large.compare(0, header.size() + 1, header.c_str(), header.size() + 1) == 0); + Require(sceHttp2GetCookie(box, "http://example.com/", nullptr, nullptr, 0) == InvalidValue); + Require(sceHttp2GetCookie(box, nullptr, nullptr, &required, 0) == InvalidValue); + Require(sceHttp2AddCookie(box, nullptr, "a=1", 3) == InvalidValue); + Require(sceHttp2AddCookie(box, "http://example.com/", nullptr, 3) == InvalidValue); + Require(sceHttp2AddCookie(box, "http://example.com/", "a=1", 0) == InvalidValue); + Require(sceHttp2AddCookie(box, "http:///path", "a=1", 3) == Rejected); + Require(sceHttp2AddCookie(box, "", "a=1", 3) == Rejected); + Require(sceHttp2GetCookieStats(box, nullptr) == InvalidValue); + Require(sceHttp2GetCookieStats(0, nullptr) == InvalidValue); + + Require(sceHttp2CookieFlush(box) == 0); + Require(NoMatch(box, "http://example.com/")); + stats = Stats(box); + Require(stats.currentInuseNum == 0 && stats.currentInuseSize == 0 && stats.maxInuseNum == 7); + + Require(sceHttp2SetCookieMaxSize(box, 4) == 0); + Require(Add(box, "http://example.com/", "abc=defg") == OutOfSize); + Require(Add(box, "http://example.com/", "ab=cd") == 0); + Require(Get(box, "http://example.com/") == "ab=cd"); + Require(Stats(box).currentInuseSize == 21); + Require(sceHttp2SetCookieMaxSize(box, 0) == 0); + Require(Add(box, "http://example.com/", "x=1") == OutOfSize); + Require(sceHttp2SetCookieMaxSize(box, 4096) == 0); + + Require(sceHttp2SetCookieMaxNumPerDomain(box, 2) == 0); + Require(Add(box, "http://example.com/", "c=1") == 0); + Require(Get(box, "http://example.com/") == "c=1; ab=cd"); + Require(Add(box, "http://example.com/", "d=1") == 0); + Require(Get(box, "http://example.com/") == "d=1; c=1"); + Require(Add(box, "http://example.com/", "e=1") == 0); + Require(Get(box, "http://example.com/") == "e=1; d=1"); + Require(Add(box, "http://other.com/", "o=1") == 0); + Require(sceHttp2SetCookieMaxNum(box, 2) == 0); + Require(Add(box, "http://third.com/", "t=1") == 0); + Require(NoMatch(box, "http://example.com/")); + Require(NoMatch(box, "http://other.com/")); + Require(Get(box, "http://third.com/") == "t=1"); + stats = Stats(box); + Require(stats.currentInuseNum == 1 && stats.removedNum == 5); + + const int defaults = sceHttp2CreateCookieBox(0x10000); + Require(defaults > 0); + Require(Add(defaults, "http://big.test/", "n=" + std::string(4096, 'v')) == OutOfSize); + Require(Add(defaults, "http://big.test/", "n=" + std::string(4095, 'v')) == 0); + Require(FillDomain(defaults, 50) == 0); + Require(Get(defaults, "http://example.com/").compare(0, 6, "c1=1; ") == 0); + for (int i = 50; i < 60; ++i) Require(Add(defaults, "http://example.com/", "c" + std::to_string(i) + "=1") == 0); + const std::string kept = Get(defaults, "http://example.com/"); + Require(kept.compare(0, 7, "c11=1; ") == 0 && kept.compare(kept.size() - 5, 5, "c10=1") == 0 && kept.find("c0=") == std::string::npos); + stats = Stats(defaults); + Require(stats.currentInuseNum == 51 && stats.removedNum == 10); + + const int noTotal = sceHttp2CreateCookieBox(0x10000); + Require(noTotal > 0 && sceHttp2SetCookieMaxNum(noTotal, 0) == 0 && FillDomain(noTotal, 60) == 0); + stats = Stats(noTotal); + Require(stats.currentInuseNum == 1 && stats.removedNum == 59); + const int noPerDomain = sceHttp2CreateCookieBox(0x10000); + Require(noPerDomain > 0 && sceHttp2SetCookieMaxNumPerDomain(noPerDomain, 0) == 0 && FillDomain(noPerDomain, 60) == 0); + stats = Stats(noPerDomain); + Require(stats.currentInuseNum == 1 && stats.removedNum == 59); + Require(Get(noPerDomain, "http://example.com/") == "c59=1"); + + Require(sceHttp2AddCookie(0, "http://example.com/", "a=1", 3) == InvalidId); + Require(sceHttp2SetCookieMaxNum(0x7fffffff, 1) == InvalidId); + Require(sceHttp2SetCookieMaxNumPerDomain(-1, 1) == InvalidId); + Require(sceHttp2SetCookieMaxSize(0, 1) == InvalidId); + Require(sceHttp2GetCookie(0, "http://example.com/", nullptr, &required, 0) == InvalidId); + Http2CookieStats ignored{}; + Require(sceHttp2GetCookieStats(0, &ignored) == InvalidId); + + const int other = sceHttp2CreateCookieBox(0x10000); + Require(other > 0 && NoMatch(other, "http://example.com/")); + Require(sceHttp2DeleteCookieBox(box) == 0); + Require(sceHttp2GetCookieStats(box, &ignored) == InvalidId); + Require(sceHttp2AddCookie(box, "http://example.com/", "a=1", 3) == InvalidId); + Require(sceHttp2DeleteCookieBox(other) == 0); + Require(sceHttp2DeleteCookieBox(defaults) == 0); + Require(sceHttp2DeleteCookieBox(noTotal) == 0); + Require(sceHttp2DeleteCookieBox(noPerDomain) == 0); +} 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/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/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 1e57a5b542..1ef5aecbf7 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -55,6 +55,7 @@ static constexpr int GuestEfault = 14; static constexpr int GuestEinval = 22; static constexpr int GuestEnotty = 25; static constexpr int GuestEagain = 35; +static constexpr int GuestEopnotsupp = 45; static constexpr int GuestEoverflow = 84; static constexpr std::uint64_t NonblockingRequest = 0x8004667e; static constexpr std::uint64_t ReadableRequest = 0x4004667f; @@ -121,18 +122,27 @@ static void SocketsKeepTheirPath() { Require(sceKernelFcntl(duplicate, GetFlags) == (ReadWrite | NonBlocking)); Require(sceKernelFcntl(duplicate, SetFlags, 0) == 0); Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); - const int closing = sceKernelFcntl(socket, DuplicateCloseOnExec, duplicate + 1); + Require(sceKernelFcntl(socket, DuplicateCloseOnExec, duplicate + 1) == SceEnotsup); + RequireFailure(fcntl_nid_postfix(socket, DuplicateCloseOnExec, duplicate + 1), GuestEopnotsupp); + const int closing = sceKernelFcntl(socket, DuplicateCommand, duplicate + 1); Require(closing >= duplicate + 1); + Require(sceKernelFcntl(closing, SetDescriptorFlags, 1) == 0); Require(sceKernelFcntl(closing, GetDescriptorFlags) == 1); Require(sceKernelFcntl(socket, SetDescriptorFlags, 0) == 0); Require(sceKernelFcntl(socket, GetDescriptorFlags) == 0); Require(fcntl_nid_postfix(socket, Duplicate2Command, closing) == closing); Require(sceKernelFcntl(closing, GetDescriptorFlags) == 0); - Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, closing) == closing); - Require(sceKernelFcntl(closing, GetDescriptorFlags) == 1); + for (const int target : {closing, socket, -1, -2}) { + const int copy = sceKernelFcntl(socket, Duplicate2CloseOnExec, target); + Require(copy >= 0 && copy != target && copy != socket && copy != closing); + Require(sceKernelFcntl(copy, GetDescriptorFlags) == 0 && sceKernelFcntl(copy, GetFlags) == ReadWrite); + Require(close_nid_postfix(copy) == 0); + const int posix = fcntl_nid_postfix(socket, Duplicate2CloseOnExec, target); + Require(posix >= 0 && posix != target && posix != socket && close_nid_postfix(posix) == 0); + } + Require(sceKernelFcntl(closing, GetDescriptorFlags) == 0); Require(sceKernelFcntl(socket, GetDescriptorFlags) == 0); - Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, socket) == socket); - Require(sceKernelFcntl(socket, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, SetDescriptorFlags, 1) == 0); Require(sceKernelFcntl(socket, Duplicate2Command, socket) == socket); Require(sceKernelFcntl(socket, GetDescriptorFlags) == 1); Require(sceKernelFcntl(socket, SetDescriptorFlags, 0) == 0); @@ -142,10 +152,19 @@ static void SocketsKeepTheirPath() { Require(fcntl_nid_postfix(socket, Duplicate2Command, pipe[0]) == pipe[0]); Require(sceKernelFcntl(pipe[0], GetFlags) == ReadWrite); Require(sceKernelFcntl(pipe[0], GetDescriptorFlags) == 0); - Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, pipe[1]) == pipe[1]); - Require(sceKernelFcntl(pipe[1], GetFlags) == ReadWrite); - Require(sceKernelFcntl(pipe[1], GetDescriptorFlags) == 1); + const int pipeCopy = sceKernelFcntl(socket, Duplicate2CloseOnExec, pipe[1]); + Require(pipeCopy >= 0 && pipeCopy != pipe[1] && sceKernelFcntl(pipeCopy, GetDescriptorFlags) == 0 && close_nid_postfix(pipeCopy) == 0); + Require(sceKernelFcntl(pipe[1], GetDescriptorFlags) == 0); + for (const int end : pipe) { + const int copy = fcntl_nid_postfix(end, Duplicate2CloseOnExec, 300); + Require(copy >= 0 && copy != end && copy != 300 && fcntl_nid_postfix(copy, GetDescriptorFlags) == 0 && close_nid_postfix(copy) == 0); + } + for (const int end : pipe) RequireFailure(fcntl_nid_postfix(end, DuplicateCloseOnExec, 0), GuestEopnotsupp); Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); + const int shared = shm_open_nid_postfix(reinterpret_cast(1), ReadWrite, 0600); + Require(shared >= 0); + RequireFailure(fcntl_nid_postfix(shared, DuplicateCloseOnExec, 0), GuestEopnotsupp); + Require(close_nid_postfix(shared) == 0); const int random = sceKernelOpen("/dev/urandom", ReadOnly, 0); Require(random >= 0); Require(fcntl_nid_postfix(socket, Duplicate2Command, random) == random); @@ -155,19 +174,22 @@ static void SocketsKeepTheirPath() { Require(file == random); char byte = 0; Require(sceKernelRead(file, &byte, 1) == 0); - Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, file) == file); - Require(sceKernelFcntl(file, GetFlags) == ReadWrite); - Require(sceKernelFcntl(file, GetDescriptorFlags) == 1); + const int fileCopy = sceKernelFcntl(file, Duplicate2CloseOnExec, 300); + Require(fileCopy >= 0 && fileCopy != file && fileCopy != 300); + Require(sceKernelFcntl(fileCopy, GetDescriptorFlags) == 0 && sceKernelFcntl(fileCopy, GetFlags) == ReadWrite); + Require(sceKernelLseek(file, 1, 0) == 1 && sceKernelLseek(fileCopy, 0, 1) == 1); + Require(close_nid_postfix(fileCopy) == 0); + RequireFailure(fcntl_nid_postfix(-1, Duplicate2CloseOnExec, 300), GuestEbadf); + RequireFailure(fcntl_nid_postfix(9999, Duplicate2CloseOnExec, 300), GuestEbadf); + Require(sceKernelFcntl(9999, Duplicate2CloseOnExec, 300) == SceEbadf); Require(close_nid_postfix(file) == 0); std::remove("kernel_fcntl_socket_dup2.bin"); RequireFailure(fcntl_nid_postfix(socket, DuplicateCommand, -1), GuestEinval); - RequireFailure(fcntl_nid_postfix(socket, DuplicateCloseOnExec, -1), GuestEinval); + RequireFailure(fcntl_nid_postfix(socket, DuplicateCloseOnExec, -1), GuestEopnotsupp); RequireFailure(fcntl_nid_postfix(socket, Duplicate2Command, -1), GuestEbadf); - RequireFailure(fcntl_nid_postfix(socket, Duplicate2CloseOnExec, -1), GuestEbadf); Require(sceKernelFcntl(socket, DuplicateCommand, -1) == SceEinval); - Require(sceKernelFcntl(socket, DuplicateCloseOnExec, -1) == SceEinval); + Require(sceKernelFcntl(socket, DuplicateCloseOnExec, -1) == SceEnotsup); Require(sceKernelFcntl(socket, Duplicate2Command, -1) == SceEbadf); - Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, -1) == SceEbadf); Require(sceKernelFcntl(socket, SetFlags, 0x8000) == SceEnotsup); Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); Require(sceKernelFcntl(socket, 5) == SceEinval); @@ -218,9 +240,13 @@ static void FileFlagsRoundTrip() { Require(fcntl_nid_postfix(duplicate, GetDescriptorFlags) == 0); Require(fcntl_nid_postfix(file, SetFlags, NonBlocking) == 0); Require(fcntl_nid_postfix(duplicate, GetFlags) == (ReadWrite | NonBlocking)); - const int closing = fcntl_nid_postfix(file, DuplicateCloseOnExec, duplicate); - Require(closing > duplicate); - Require(fcntl_nid_postfix(closing, GetDescriptorFlags) == 1); + for (const int minimum : {0, duplicate, 200, -1}) { + RequireFailure(fcntl_nid_postfix(file, DuplicateCloseOnExec, minimum), GuestEopnotsupp); + Require(sceKernelFcntl(file, DuplicateCloseOnExec, minimum) == SceEnotsup); + } + RequireFailure(fcntl_nid_postfix(-1, DuplicateCloseOnExec, 0), GuestEbadf); + RequireFailure(fcntl_nid_postfix(9999, DuplicateCloseOnExec, 0), GuestEbadf); + Require(sceKernelFcntl(-1, DuplicateCloseOnExec, 0) == SceEbadf); Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 0); Require(write_nid_postfix(file, "abcde", 5) == 5); @@ -250,7 +276,7 @@ static void FileFlagsRoundTrip() { lock.type = 7; RequireFailure(fcntl_nid_postfix(file, SetLock, &lock), GuestEinval); - for (const int descriptor : {closing, duplicate, file}) Require(close_nid_postfix(descriptor) == 0); + for (const int descriptor : {duplicate, file}) Require(close_nid_postfix(descriptor) == 0); RequireFailure(fcntl_nid_postfix(file, GetDescriptorFlags), GuestEbadf); RequireFailure(fcntl_nid_postfix(file, DuplicateCommand, 0), GuestEbadf); RequireFailure(ioctl_nid_postfix(file, ReadableRequest, &readable), GuestEbadf); @@ -283,7 +309,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 ffe0e31409..e74074c871 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -47,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()); @@ -361,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); @@ -502,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); @@ -526,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); } @@ -692,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); @@ -982,6 +972,29 @@ int main() { std::filesystem::create_directory(root / "nonempty"); { std::ofstream out(root / "nonempty" / "f.bin"); out << 'x'; } Require(unlinkat_nid_postfix(base, "nonempty", 0x800) == -1 && Error() == 66); +#ifdef _WIN32 + const auto fullJunctionTarget = root / "junction-full-target"; + Require(std::filesystem::create_directory(fullJunctionTarget)); + { std::ofstream out(fullJunctionTarget / "inside.txt"); out << 'x'; } + const auto fullJunctionCommand = L"cmd /c mklink /J \"" + (root / "junction-full").wstring() + L"\" \"" + std::filesystem::absolute(fullJunctionTarget).wstring() + L"\""; + Require(_wsystem(fullJunctionCommand.c_str()) == 0); + Require(unlinkat_nid_postfix(base, "junction-full", 0x800) == -1 && Error() == 20); + Require(std::filesystem::is_regular_file(fullJunctionTarget / "inside.txt")); + Require(unlinkat_nid_postfix(base, "junction-full", 0) == 0); + Require(!std::filesystem::exists(std::filesystem::symlink_status(root / "junction-full"))); + Require(std::filesystem::is_regular_file(fullJunctionTarget / "inside.txt")); + Require(std::filesystem::remove(fullJunctionTarget / "inside.txt") && std::filesystem::remove(fullJunctionTarget)); + { std::ofstream out(root / "symlink-target.bin"); out << 'x'; } + const int madeLink = symlink_nid_postfix("symlink-target.bin", (root / "symlink-file").string().c_str()); + if (madeLink == 0) { + Require(unlinkat_nid_postfix(base, "symlink-file", 0) == 0); + Require(!std::filesystem::exists(std::filesystem::symlink_status(root / "symlink-file"))); + Require(std::filesystem::is_regular_file(root / "symlink-target.bin")); + Require(std::filesystem::remove(root / "symlink-target.bin")); + } else { + Require(std::filesystem::remove(root / "symlink-target.bin")); + } +#endif Require(unlinkat_nid_postfix(base, "nonempty/f.bin", 0) == 0 && !std::filesystem::exists(root / "nonempty" / "f.bin")); Require(unlinkat_nid_postfix(base, "nonempty", 0x800) == 0 && !std::filesystem::exists(root / "nonempty")); { std::ofstream out(root / "to_unlink.bin"); out << 'x'; } @@ -1099,9 +1112,37 @@ int main() { Require((std::filesystem::status(modeFile).permissions() & std::filesystem::perms::all) == readable); Require(RemoveSymlink(modeLink)); #endif + const auto chmodTarget = (root / "chmod-target.txt").string(); + { std::ofstream file(chmodTarget); file << "x"; } + const auto chmodLink = (root / "chmod-link.txt").string(); + const auto chmodDangling = (root / "chmod-dangling.txt").string(); + const auto chmodLoopA = (root / "chmod-loop-a").string(); + const auto chmodLoopB = (root / "chmod-loop-b").string(); + if (symlink_nid_postfix("chmod-target.txt", chmodLink.c_str()) == 0) { + Require(symlink_nid_postfix("chmod-missing.txt", chmodDangling.c_str()) == 0); + Require(symlink_nid_postfix("chmod-loop-b", chmodLoopA.c_str()) == 0 && symlink_nid_postfix("chmod-loop-a", chmodLoopB.c_str()) == 0); + const auto writable = [&] { + FileStat linked{}; + Require(stat_nid_postfix(chmodTarget.c_str(), &linked) == 0); + return (linked.st_mode & 0222) != 0; + }; + Require(writable()); + Require(fchmodat_nid_postfix(base, "chmod-link.txt", 0400, 0) == 0 && !writable()); + Require(fchmodat_nid_postfix(-100, chmodLink.c_str(), 0600, 0) == 0 && writable()); + Require(fchmodat_nid_postfix(base, "chmod-dangling.txt", 0600, 0) == -1 && Error() == 2); + Require(fchmodat_nid_postfix(-100, chmodDangling.c_str(), 0600, 0) == -1 && Error() == 2); + Require(fchmodat_nid_postfix(base, "chmod-loop-a", 0600, 0) == -1 && Error() == 62); + Require(fchmodat_nid_postfix(base, "chmod-link.txt/child", 0600, 0) == -1 && Error() == 20); + Require(RemoveSymlink(chmodLink) && RemoveSymlink(chmodDangling) && RemoveSymlink(chmodLoopA) && RemoveSymlink(chmodLoopB)); + } 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/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/GuestKernelSysThreads.cpp b/core/libs/tests/GuestKernelSysThreads.cpp index b15c2a7bb6..4134cfd6f2 100644 --- a/core/libs/tests/GuestKernelSysThreads.cpp +++ b/core/libs/tests/GuestKernelSysThreads.cpp @@ -69,14 +69,18 @@ int main(int argc, char** argv) { #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); + Require(other == 0 && isMain() == 1 && sceOther == 0 && sceIsMain() == 1); return 0; } #endif @@ -86,10 +90,15 @@ int main(int argc, char** argv) { 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; - std::thread worker([&] { other = isMain(); }); + int sceOther = -1; + std::thread worker([&] { + other = isMain(); + sceOther = sceIsMain(); + }); worker.join(); - Require(other == 0 && isMain() == 1 && *error() == 71); + Require(other == 0 && isMain() == 1 && sceOther == 0 && sceIsMain() == 1 && *error() == 71); return 0; } diff --git a/core/libs/tests/GuestKernelSysUuid.cpp b/core/libs/tests/GuestKernelSysUuid.cpp new file mode 100644 index 0000000000..8977988dbb --- /dev/null +++ b/core/libs/tests/GuestKernelSysUuid.cpp @@ -0,0 +1,60 @@ +#include "prx/libkernel_sys/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "uuidgen check failed at line %u (guest errno %d)\n", location.line(), *__error_nid_postfix()); + std::abort(); + } +} + +int Error() { + return *__error_nid_postfix(); +} + +std::uint64_t Timestamp(const GuestUuid& uuid) { + return (static_cast(uuid.timeHiAndVersion & 0x0fff) << 48) | (static_cast(uuid.timeMid) << 32) | uuid.timeLow; +} + +std::uint16_t Sequence(const GuestUuid& uuid) { + return static_cast(((uuid.clockSeqHiAndReserved & 0x3f) << 8) | uuid.clockSeqLow); +} + +} + +int main() { + std::array first{}; + std::array second{}; + Require(uuidgen_nid_postfix(first.data(), 0) == -1 && Error() == 22); + Require(uuidgen_nid_postfix(first.data(), 2049) == -1 && Error() == 22); + Require(uuidgen_nid_postfix(nullptr, 1) == -1 && Error() == 14); + Require(uuidgen_nid_postfix(first.data(), static_cast(first.size())) == 0); + Require(uuidgen_nid_postfix(second.data(), static_cast(second.size())) == 0); + for (const auto& uuid : first) { + Require((uuid.timeHiAndVersion >> 12) == 1); + Require((uuid.clockSeqHiAndReserved & 0xc0) == 0x80); + Require((uuid.node[0] & 0x01) != 0); + Require(std::memcmp(uuid.node, first[0].node, sizeof(uuid.node)) == 0); + Require(Sequence(uuid) == Sequence(first[0])); + } + Require(Timestamp(first[1]) == Timestamp(first[0]) + 1 && Timestamp(first[2]) == Timestamp(first[0]) + 2); + Require(std::memcmp(second[0].node, first[0].node, sizeof(first[0].node)) == 0); + const bool later = Timestamp(second[0]) > Timestamp(first[2]) && Sequence(second[0]) == Sequence(first[0]); + const bool bumped = Timestamp(second[0]) <= Timestamp(first[2]) && Sequence(second[0]) == ((Sequence(first[0]) + 1) & 0x3fff); + Require(later || bumped); + const auto unixTicks = static_cast(std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count() / 100); + const auto gregorianTicks = unixTicks + 0x01B21DD213814000ULL; + const auto stamp = Timestamp(first[0]); + Require(stamp + 600000000ULL > gregorianTicks && stamp < gregorianTicks + 600000000ULL); +} diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 997a75c727..beac08a160 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -900,6 +900,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]{}; @@ -1429,6 +1482,7 @@ int main() { EdgeTriggeredDispatch(); #endif DisableAndDispatch(); + ReportedKnotesMoveToTail(); Errors(); LowWaterMarks(); FileWatch(); diff --git a/core/libs/tests/GuestLibcThreads.cpp b/core/libs/tests/GuestLibcThreads.cpp index 51638d8749..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(); } @@ -275,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{}; @@ -508,6 +582,7 @@ static void ThreadHelpers() { } int main() { + ThreadExitLifecycle(); KernelKeys(); VersionedSync(); ForwardedCalls(); 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/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/GuestLibreSslRsa.cpp b/core/libs/tests/GuestLibreSslRsa.cpp new file mode 100644 index 0000000000..80af7c5b46 --- /dev/null +++ b/core/libs/tests/GuestLibreSslRsa.cpp @@ -0,0 +1,118 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +struct TestBignum { + std::uint64_t* d; + int top; + int dmax; + int neg; + int flags; +}; + +struct TestRsaMethod { + const char* name; + void* operations[8]; + int flags; + char* appData; + void* sign; + void* verify; + void* keygen; +}; + +struct TestRsa { + int pad; + std::int64_t version; + const TestRsaMethod* meth; + void* engine; + void* n; + void* rest[15]; +}; + +static_assert(offsetof(TestRsaMethod, flags) == 72); +static_assert(offsetof(TestRsa, meth) == 16 && offsetof(TestRsa, n) == 32 && sizeof(TestRsa) == 160); + +static const LibreSslRsa* Rsa(const TestRsa* rsa) { + return reinterpret_cast(rsa); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "LibreSSL RSA 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 TestFlags() { + TestRsaMethod method {}; + TestRsa rsa {}; + rsa.meth = &method; + Require(RSA_flags_nid_postfix(nullptr) == 0); + Require(RSA_flags_nid_postfix(Rsa(&rsa)) == 0); + method.flags = 0x0400; + Require(RSA_flags_nid_postfix(Rsa(&rsa)) == 0x0400); + method.flags = -1; + Require(RSA_flags_nid_postfix(Rsa(&rsa)) == -1); + rsa.meth = nullptr; + RequireThrows([&] { RSA_flags_nid_postfix(Rsa(&rsa)); }); +} + +static int Size(TestBignum* modulus) { + TestRsa rsa {}; + rsa.n = modulus; + return RSA_size_nid_postfix(Rsa(&rsa)); +} + +static void TestSize() { + std::uint64_t words[32] {}; + TestBignum modulus {words, 0, 32, 0, 0}; + Require(Size(&modulus) == 0); + modulus.d = nullptr; + Require(Size(&modulus) == 0); + modulus.d = words; + modulus.top = 1; + words[0] = 1; + Require(Size(&modulus) == 1); + words[0] = 0xFF; + Require(Size(&modulus) == 1); + words[0] = 0x100; + Require(Size(&modulus) == 2); + words[0] = 0x8000000000000000ull; + Require(Size(&modulus) == 8); + modulus.top = 2; + words[1] = 1; + Require(Size(&modulus) == 9); + modulus.top = 32; + words[31] = 0xC000000000000000ull; + Require(Size(&modulus) == 256); + modulus.neg = 1; + Require(Size(&modulus) == 256); + modulus.neg = 0; + modulus.top = 16; + words[15] = 0x00FFFFFFFFFFFFFFull; + Require(Size(&modulus) == 127); + words[15] = 0; + Require(Size(&modulus) == 120); + modulus.top = -1; + RequireThrows([&] { Size(&modulus); }); + modulus.top = 1; + modulus.d = nullptr; + RequireThrows([&] { Size(&modulus); }); + RequireThrows([] { Size(nullptr); }); + RequireThrows([] { RSA_size_nid_postfix(nullptr); }); +} + +int main() { + TestFlags(); + TestSize(); + std::puts("libSceLibreSsl RSA tests passed"); +} 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 64a41353cf..bfcab6cb6a 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -8,12 +8,16 @@ #include #include #include +#include #include #include +#include +#include #include extern "C" { extern GuestLocale::Implementation* _ZSt21_sceLibcClassicLocale_nid_postfix; +extern std::uint64_t _ZNSt8numpunctIcE2idE_nid_postfix; extern std::uint64_t _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix; extern std::uint64_t _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix; @@ -23,6 +27,8 @@ 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]; } @@ -72,12 +78,22 @@ static void CheckCodecvtWideVtableObject() { } static void CheckCollateVtableObject() { - for (std::uintptr_t& slot : _ZTVSt7collateIcE_nid_postfix) { - Require(slot == 0); - slot = 0x1111; - Require(slot == 0x1111); - slot = 0; - } + Require(_ZTVSt7collateIcE_nid_postfix[0] == 0 && _ZTVSt7collateIcE_nid_postfix[1] == 0); + for (std::size_t index = 2; index < std::size(_ZTVSt7collateIcE_nid_postfix); ++index) Require(_ZTVSt7collateIcE_nid_postfix[index] != 0); + const auto* vtable = reinterpret_cast(&_ZTVSt7collateIcE_nid_postfix[2]); + GuestLocale::CollateFacet facet{}; + facet.base.references = 0; + vtable->facet.destroy(&facet.base); + vtable->facet.retain(&facet.base); + Require(facet.base.references == 1); + Require(vtable->facet.release(&facet.base) == &facet.base && facet.base.references == 0); + Require(vtable->compare(&facet, "abc", "abc" + 3, "abd", "abd" + 3) < 0); + Require(vtable->compare(&facet, "abc", "abc" + 3, "abc", "abc" + 3) == 0); + Require(vtable->compare(&facet, "abd", "abd" + 3, "abc", "abc" + 3) > 0); + GuestLocale::String result{}; + Require(vtable->transform(&result, &facet, "abc", "abc" + 3) == &result); + Require(result.size == 3 && result.capacity == 15 && std::memcmp(result.buffer, "abc", 3) == 0); + Require(vtable->hash(&facet, "abc", "abc" + 3) == -1792535898324117685LL); } static void CheckGuestCalls() { @@ -138,7 +154,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)); @@ -208,6 +224,28 @@ static void CheckStreamWidth() { RequireException([&] { manipulator.function(nullptr, manipulator.argument); }); } +static void CheckStreamPrecision() { + struct GuardedStream { + std::uint64_t before = 0x12345678; + GuestLocale::IosBase stream{}; + std::uint64_t after = 0x87654321; + } object; + object.stream.flags = 0x4000; + object.stream.width = 5; + const auto manipulator = _ZSt12setprecisioni_nid_postfix(12); + Require(manipulator.argument == 12 && manipulator.function != nullptr); + Require(manipulator.function(&object.stream, manipulator.argument) == &object.stream); + Require(object.stream.precision == 12 && object.stream.flags == 0x4000 && object.stream.width == 5); + Require(object.before == 0x12345678 && object.after == 0x87654321); + const auto zero = _ZSt12setprecisioni_nid_postfix(0); + zero.function(&object.stream, zero.argument); + Require(object.stream.precision == 0); + const auto negative = _ZSt12setprecisioni_nid_postfix(-3); + negative.function(&object.stream, negative.argument); + Require(object.stream.precision == -3); + RequireException([&] { manipulator.function(nullptr, manipulator.argument); }); +} + static void CheckStandardStreams() { struct GuardedStream { std::uint64_t before = 0x12345678; @@ -291,6 +329,132 @@ static void CheckWideNumPutVtableObject() { } } +struct TestStreambuf { + GuestLocale::Streambuf buffer; + std::string accepted; + int offered; + int capacity; +}; + +int APS5_VABI TestOverflow(GuestLocale::Streambuf* self, int character) { + auto* stream = reinterpret_cast(self); + ++stream->offered; + if (static_cast(stream->accepted.size()) >= stream->capacity) return -1; + stream->accepted += static_cast(character); + return character; +} + +const GuestLocale::StreambufVtable g_testStreambufVtable{{}, TestOverflow}; + +static void CheckTimePut() { + const GuestLocale::Implementation* locale = _ZSt21_sceLibcClassicLocale_nid_postfix; + GuestLocale::Facet* facet = nullptr; + Require(_ZNSt8time_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(&facet, &locale) == 5); + const auto put = reinterpret_cast(facet->vtable)->put; + GuestTm time{}; + time.tm_year = 126; + time.tm_mon = 9; + time.tm_mday = 2; + time.tm_hour = 13; + time.tm_min = 5; + time.tm_sec = 9; + time.tm_wday = 5; + time.tm_yday = 274; + char storage[8]; + char* next = storage; + char* end = storage + sizeof(storage); + TestStreambuf stream{{&g_testStreambufVtable, {}, &next, {}, &end}, {}, 0, 100}; + GuestLocale::OutputIterator out = put(facet, {false, &stream.buffer}, nullptr, '*', &time, 'Y', 0); + Require(!out.failed && out.buffer == &stream.buffer && next == storage + 4 && std::memcmp(storage, "2026", 4) == 0 && stream.offered == 0); + out = put(facet, out, nullptr, '*', &time, 'c', 0); + Require(!out.failed && next == end && std::memcmp(storage + 4, "Fri ", 4) == 0 && stream.accepted == "Oct 2 13:05:09 2026"); + stream.accepted.clear(); + out = put(facet, out, nullptr, '*', &time, 'Y', 'E'); + out = put(facet, out, nullptr, '*', &time, 'y', 'O'); + out = put(facet, out, nullptr, '*', &time, '\0', 0); + out = put(facet, out, nullptr, '*', &time, 'k', 0); + Require(!out.failed && stream.accepted == "202626%%k"); + stream.capacity = static_cast(stream.accepted.size()) + 1; + stream.offered = 0; + out = put(facet, out, nullptr, '*', &time, 'T', 0); + Require(out.failed && stream.offered == 8 && stream.accepted == "202626%%k1"); + next = nullptr; + stream.offered = 0; + out = put(facet, {false, &stream.buffer}, nullptr, '*', &time, 'H', 0); + Require(out.failed && stream.offered == 2); + out = put(facet, {false, nullptr}, nullptr, '*', &time, 'H', 0); + Require(out.failed && out.buffer == nullptr); + facet->vtable->deleteObject(facet); +} + +GuestLocale::String* AssignShortString(GuestLocale::String* result, const char* text) { + *result = {}; + result->size = std::strlen(text); + result->capacity = 15; + std::memcpy(result->buffer, text, result->size); + return result; +} + +void APS5_VABI TestFacetNop(GuestLocale::Facet*) {} +GuestLocale::Facet* APS5_VABI TestFacetRelease(GuestLocale::Facet*) { return nullptr; } +char APS5_VABI TestDecimalPoint(const GuestLocale::NumpunctFacet*) { return ','; } +char APS5_VABI TestThousandsSep(const GuestLocale::NumpunctFacet*) { return '.'; } +GuestLocale::String* APS5_VABI TestGrouping(GuestLocale::String* result, const GuestLocale::NumpunctFacet*) { return AssignShortString(result, "\1\2"); } +GuestLocale::String* APS5_VABI TestFalsename(GuestLocale::String* result, const GuestLocale::NumpunctFacet*) { return AssignShortString(result, "no"); } +GuestLocale::String* APS5_VABI TestTruename(GuestLocale::String* result, const GuestLocale::NumpunctFacet*) { return AssignShortString(result, "yes"); } +const GuestLocale::NumpunctVtable g_testNumpunctVtable{{TestFacetNop, TestFacetNop, TestFacetNop, TestFacetRelease}, TestDecimalPoint, TestThousandsSep, TestGrouping, TestFalsename, TestTruename}; + +template +static std::string NumPut(TPut put, GuestLocale::Implementation** locale, std::int32_t flags, std::int32_t precision, std::int32_t width) { + char storage[64]; + char* next = storage; + char* end = storage + sizeof(storage); + TestStreambuf stream{{&g_testStreambufVtable, {}, &next, {}, &end}, {}, 0, 0}; + GuestLocale::IosBase ios{}; + ios.flags = flags; + ios.precision = precision; + ios.width = width; + ios.locale = locale; + const GuestLocale::OutputIterator out = put(GuestLocale::OutputIterator{false, &stream.buffer}, &ios); + Require(!out.failed && out.buffer == &stream.buffer && ios.width == 0 && stream.offered == 0); + return std::string(storage, next); +} + +static void CheckNumPut() { + GuestLocale::Implementation** classic = &_ZSt21_sceLibcClassicLocale_nid_postfix; + const GuestLocale::Implementation* locale = _ZSt21_sceLibcClassicLocale_nid_postfix; + GuestLocale::Facet* facet = nullptr; + Require(_ZNSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(&facet, &locale) == 4); + const auto* put = reinterpret_cast(facet->vtable); + const double infinity = std::numeric_limits::infinity(); + Require(NumPut([&](auto out, auto* ios) { return put->putLong(facet, out, ios, '*', -42); }, classic, 0x408, 6, 8) == "-*****42"); + Require(NumPut([&](auto out, auto* ios) { return put->putLong(facet, out, ios, '*', 255); }, classic, 0x2c8, 6, 10) == "0X******FF"); + Require(NumPut([&](auto out, auto* ios) { return put->putUnsignedLong(facet, out, ios, '*', std::numeric_limits::max()); }, classic, 0x20, 6, 0) == "1777777777777777777777"); + Require(NumPut([&](auto out, auto* ios) { return put->putLongLong(facet, out, ios, '*', std::numeric_limits::min()); }, classic, 0x2, 6, 25) == "-9223372036854775808*****"); + Require(NumPut([&](auto out, auto* ios) { return put->putBool(facet, out, ios, '*', true); }, classic, 0x8002, 6, 7) == "true***"); + Require(NumPut([&](auto out, auto* ios) { return put->putBool(facet, out, ios, '*', false); }, classic, 0, 6, 0) == "0"); + Require(NumPut([&](auto out, auto* ios) { return put->putPointer(facet, out, ios, '*', reinterpret_cast(0x1234)); }, classic, 0, 6, 6) == "**1234"); + Require(NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', 1234567.891); }, classic, 0, 6, 0) == "1.23457e+06"); + Require(NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', 1234567.891); }, classic, 0x1000, 2, 0) == "1234567.89"); + Require(NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', -0.000123); }, classic, 0xa00, 3, 12) == "**-1.230E-04"); + Require(NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', -1.5); }, classic, 0x1808, 6, 12) == "-***0x1.8p+0"); + Require(NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', 12345678901234.0); }, classic, 0x1000, 0, 0) == "12345678901234"); + Require(NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', -infinity); }, classic, 0x400, 6, 0) == "-inf"); + Require(NumPut([&](auto out, auto* ios) { return put->putLongDouble(facet, out, ios, '*', -2.25L); }, classic, 0, 3, 0) == "-2.25"); + RequireException([&] { NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', 1.0); }, classic, 0x1000, -45, 0); }); + GuestLocale::Facet punct{&g_testNumpunctVtable.facet, 1, 0}; + std::vector facets(_ZNSt8numpunctIcE2idE_nid_postfix + 1); + facets.back() = &punct; + GuestLocale::Implementation custom{{nullptr, 1, 0}, facets.data(), facets.size(), 0, false, "C"}; + GuestLocale::Implementation* customPointer = &custom; + Require(NumPut([&](auto out, auto* ios) { return put->putLong(facet, out, ios, '*', 1234567); }, &customPointer, 0, 6, 0) == "12.34.56.7"); + Require(NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', 1234567.5); }, &customPointer, 0x1000, 1, 0) == "12.34.56.7,5"); + Require(NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', infinity); }, &customPointer, 0x1000, 1, 0) == "inf"); + Require(NumPut([&](auto out, auto* ios) { return put->putDouble(facet, out, ios, '*', -infinity); }, &customPointer, 0, 6, 0) == "-inf"); + Require(NumPut([&](auto out, auto* ios) { return put->putBool(facet, out, ios, '*', true); }, &customPointer, 0x8000, 6, 5) == "**yes"); + facet->vtable->deleteObject(facet); +} + static void CheckFacetGetcat() { const GuestLocale::Implementation* locale = _ZSt21_sceLibcClassicLocale_nid_postfix; GuestLocale::Facet* codecvt = nullptr; @@ -335,6 +499,15 @@ 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); @@ -344,6 +517,15 @@ static void CheckTimeGetVtableObject() { } } +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); @@ -360,6 +542,7 @@ int main() { CheckCharacterConversions(); CheckStreamDestruction(); CheckStreamWidth(); + CheckStreamPrecision(); CheckStandardStreams(); CheckWideCaseTranslation(); CheckInputFacetObjects(); @@ -368,10 +551,14 @@ int main() { CheckFacetGetcat(); CheckCollateVtableObject(); CheckTimeGetVtableObject(); + CheckTimeGetWideVtableObject(); + CheckNumGetWideVtableObject(); _Locksyslock_nid_postfix(); _Locksyslock_nid_postfix(); _Unlocksyslock_nid_postfix(); _Unlocksyslock_nid_postfix(); + CheckTimePut(); + CheckNumPut(); std::cout << "Guest locale checks passed\n"; } 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 7c6bd1e44e..ea2219eb63 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -542,6 +542,65 @@ 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; @@ -571,6 +630,110 @@ static void CheckLog1pConsoleBits() { } } +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}, @@ -924,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); @@ -937,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() { @@ -1268,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(); @@ -1606,6 +1914,11 @@ int main() { 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); @@ -1693,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()); @@ -1728,6 +2042,7 @@ 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(); @@ -1744,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 db7361070f..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)); 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 f31f21accc..3b28008f19 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -1569,7 +1569,7 @@ static void CheckAliasRefusesPagesThatAreNotSharedViews() { } #endif -#if defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) +#if defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) && !defined(ANYPS5_ENABLE_TSAN) static std::size_t LockedKilobytes() { std::ifstream status("/proc/self/status"); std::string line; @@ -1593,7 +1593,7 @@ static void CheckMlock() { Require(sceKernelMapFlexibleMemory(&mapped, length, 3, 0) == 0); auto* bytes = static_cast(mapped); Require(sceKernelMlock_nid_postfix(mapped, 0) == 0); -#if defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) +#if defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) && !defined(ANYPS5_ENABLE_TSAN) const auto lockedBefore = LockedKilobytes(); #endif Require(sceKernelMlock_nid_postfix(bytes + 1, length - page) == 0); @@ -1604,7 +1604,7 @@ static void CheckMlock() { Require(GetProcessWorkingSetSizeEx(GetCurrentProcess(), &minimum, &maximum, &limits) && minimum >= length && maximum > minimum); Require(VirtualUnlock(mapped, length)); Require(!VirtualUnlock(mapped, length) && GetLastError() == ERROR_NOT_LOCKED); -#elif defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) +#elif defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) && !defined(ANYPS5_ENABLE_TSAN) Require(LockedKilobytes() - lockedBefore == length / 1024); #endif Require(sceKernelMlock_nid_postfix(mapped, length) == 0); @@ -1613,12 +1613,12 @@ static void CheckMlock() { Require(sceKernelMlock_nid_postfix(reinterpret_cast(std::numeric_limits::max() - page + 1), page * 2) == invalid); void* reserved = nullptr; Require(sceKernelReserveVirtualRange(&reserved, page, 0, 0) == 0); -#ifndef ANYPS5_ENABLE_ASAN +#if !defined(ANYPS5_ENABLE_ASAN) && !defined(ANYPS5_ENABLE_TSAN) Require(sceKernelMlock_nid_postfix(reserved, page) == outOfMemory); #endif Require(sceKernelMunmap(reinterpret_cast(reserved), page) == 0); Require(sceKernelMunmap(reinterpret_cast(mapped), length) == 0); -#ifndef ANYPS5_ENABLE_ASAN +#if !defined(ANYPS5_ENABLE_ASAN) && !defined(ANYPS5_ENABLE_TSAN) Require(sceKernelMlock_nid_postfix(mapped, page) == outOfMemory); #endif } 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/GuestMlock.cpp b/core/libs/tests/GuestMlock.cpp index 71883b2b4c..43b0f46d48 100644 --- a/core/libs/tests/GuestMlock.cpp +++ b/core/libs/tests/GuestMlock.cpp @@ -35,7 +35,7 @@ static void RequireLockAllUnsupported(int flags) { Require(rejected); } -#if defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) +#if defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) && !defined(ANYPS5_ENABLE_TSAN) #define HAS_LOCKED_KILOBYTES static std::size_t LockedKilobytes() { std::ifstream status("/proc/self/status"); @@ -125,7 +125,7 @@ int main() { void* reserved = nullptr; Require(sceKernelReserveVirtualRange(&reserved, page, 0, 0) == 0); -#ifndef ANYPS5_ENABLE_ASAN +#if !defined(ANYPS5_ENABLE_ASAN) && !defined(ANYPS5_ENABLE_TSAN) *__error_nid_postfix() = 0; Require(mlock_nid_postfix(reserved, page) == -1 && *__error_nid_postfix() == 12); #endif @@ -135,7 +135,7 @@ int main() { Require(sceKernelMunmap(reinterpret_cast(reserved), page) == 0); Require(sceKernelMunmap(reinterpret_cast(mapped), length) == 0); -#ifndef ANYPS5_ENABLE_ASAN +#if !defined(ANYPS5_ENABLE_ASAN) && !defined(ANYPS5_ENABLE_TSAN) *__error_nid_postfix() = 0; Require(mlock_nid_postfix(mapped, page) == -1 && *__error_nid_postfix() == 12); *__error_nid_postfix() = 0; 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/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/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/GuestNpCppWebApiIntrusivePtr.cpp b/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp index 13cc9a8491..37fb800f48 100644 --- a/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp +++ b/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp @@ -10,6 +10,7 @@ using Deleter = void (APS5_VABI*)(void*); using Construct = void (APS5_VABI*)(IntrusivePtr*); using Copy = void (APS5_VABI*)(IntrusivePtr*, const IntrusivePtr*); +using Assign = void (APS5_VABI*)(IntrusivePtr*, const IntrusivePtr*); using Destroy = void (APS5_VABI*)(IntrusivePtr*); using Access = void* (APS5_VABI*)(const IntrusivePtr*); @@ -23,6 +24,11 @@ struct DestroyCase { std::size_t referenceCountOffset; }; +struct AssignCase { + Assign assign; + std::size_t referenceCountOffset; +}; + static void* deletedObject = nullptr; static int deleterCalls = 0; @@ -92,6 +98,38 @@ static void TestCopy(const Copy copy) { Require(target.libContext == &context); } +static void TestAssign(const AssignCase& testCase) { + const std::size_t offset = testCase.referenceCountOffset; + alignas(8) unsigned char first[ObjectSize]; + alignas(8) unsigned char second[ObjectSize]; + std::memset(first, 0x5a, sizeof(first)); + std::memset(second, 0x5a, sizeof(second)); + WriteCount(first, offset, 1); + WriteCount(second, offset, 1); + int context = 0; + deleterCalls = 0; + deletedObject = nullptr; + + IntrusivePtr pointer{first, RecordDelete, &context}; + testCase.assign(&pointer, &pointer); + Require(pointer.object == first && pointer.deleter == RecordDelete && pointer.libContext == &context); + Require(ReadCount(first, offset) == 1); + + const IntrusivePtr alias{first, nullptr, nullptr}; + testCase.assign(&pointer, &alias); + Require(pointer.object == first && pointer.deleter == RecordDelete && pointer.libContext == &context); + Require(ReadCount(first, offset) == 1); + Require(deleterCalls == 0); + + const IntrusivePtr other{second, RecordDelete, &context}; + testCase.assign(&pointer, &other); + Require(pointer.object == second); + Require(ReadCount(second, offset) == 2); + Require(ReadCount(first, offset) == 0); + Require(deleterCalls == 1 && deletedObject == first); + Require(OnlyCountChanged(first, offset) && OnlyCountChanged(second, offset)); +} + static void TestDestroy(const DestroyCase& testCase) { alignas(8) unsigned char object[ObjectSize]; std::memset(object, 0x5a, sizeof(object)); @@ -227,6 +265,48 @@ static void TestAdoptedVectorBodyDestroyedThroughDeleter() { Require(vector.object == nullptr); } + +static void TestUserProfileVectorAssign() { + int context = 0; + alignas(8) unsigned char first[ObjectSize]; + alignas(8) unsigned char second[ObjectSize]; + std::memset(first, 0x5a, sizeof(first)); + std::memset(second, 0x5a, sizeof(second)); + WriteCount(first, VectorReferenceCountOffset, 1); + WriteCount(second, VectorReferenceCountOffset, 7); + deleterCalls = 0; + deletedObject = nullptr; + + IntrusivePtr target{first, RecordDelete, &context}; + const IntrusivePtr source{second, RecordDelete, &context}; + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V112BasicProfileEEEEEEaSERKSA_(&target, &source); + Require(target.object == second && target.deleter == RecordDelete && target.libContext == &context); + Require(ReadCount(first, VectorReferenceCountOffset) == 0 && ReadCount(second, VectorReferenceCountOffset) == 8); + Require(deleterCalls == 1 && deletedObject == first); + + IntrusivePtr empty{nullptr, RecordDelete, &context}; + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V112BasicProfileEEEEEEaSERKSA_(&empty, &source); + Require(empty.object == second && ReadCount(second, VectorReferenceCountOffset) == 9); + Require(OnlyCountChanged(second, VectorReferenceCountOffset)); + + IntrusivePtr presenceTarget{nullptr, RecordDelete, &context}; + const IntrusivePtr presenceSource{second, RecordDelete, &context}; + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEaSERKSA_(&presenceTarget, &presenceSource); + Require(presenceTarget.object == second && presenceTarget.deleter == RecordDelete && presenceTarget.libContext == &context); + Require(ReadCount(second, VectorReferenceCountOffset) == 10); + Require(deleterCalls == 1); +} + +static void TestUserProfileModelAndTransaction() { + Require(_ZN3sce2Np9CppWebApi11UserProfile2V112BasicProfileC1EPNS1_6Common10LibContextE() == 0); + + std::uint64_t transaction[0x20]; + std::memset(transaction, 0xa5, sizeof(transaction)); + Require(_ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(transaction) == 0); + Require(_ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv() == 0); + Require(_ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(transaction) == 0); +} + int main() { Require(sizeof(IntrusivePtr) == 0x18); @@ -243,6 +323,8 @@ int main() { _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEEC1Ev, _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEC1Ev, _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEC1Ev, }; for (const auto construct : constructors) TestConstruct(construct); @@ -281,6 +363,9 @@ int main() { _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEdeEv, _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEptEv, _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_17TitleCloudStorage2V110DataStatusEEEEEEdeEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V112BasicProfileEEEEEEptEv, }; for (const auto access : accessors) TestAccess(access); @@ -290,9 +375,19 @@ int main() { _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEEC1ERS7_, _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V57ProductEEC1ERS7_, _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEC1ERS7_, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEC1ERS7_, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEC1ERS7_, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEC1ERKS7_, }; for (const auto copy : copies) TestCopy(copy); + const AssignCase assignments[] = { + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V113PlayerSessionEEaSERS7_, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V113PlayerSessionEEEEEEaSERKSA_, VectorReferenceCountOffset}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEaSERKSA_, VectorReferenceCountOffset}, + }; + for (const auto& testCase : assignments) TestAssign(testCase); + const DestroyCase destructors[] = { {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEED1Ev, 0}, {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEED1Ev, 0}, @@ -314,11 +409,17 @@ int main() { {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V57ProductEEEEEED1Ev, VectorReferenceCountOffset}, {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEED1Ev, VectorReferenceCountOffset}, {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_17TitleCloudStorage2V110DataStatusEEEEEED1Ev, VectorReferenceCountOffset}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V112BasicProfileEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V112BasicProfileEEEEEED1Ev, VectorReferenceCountOffset}, }; for (const auto& testCase : destructors) TestDestroy(testCase); TestSharedOwnership(); TestVectorPushBackReleasesElements(); TestAdoptedVectorBodyDestroyedThroughDeleter(); + TestUserProfileVectorAssign(); + TestUserProfileModelAndTransaction(); return 0; } 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..3238425c00 100644 --- a/core/libs/tests/GuestNpGameIntent.cpp +++ b/core/libs/tests/GuestNpGameIntent.cpp @@ -2,9 +2,14 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include #include #include +#include #include +#include +#include static void Require(bool value, const char* what) { if (value) return; @@ -23,8 +28,12 @@ static bool Throws(TCall call) { } int main() { + constexpr int kNotInitialized = -2141898750; + constexpr int kInvalidArgument = -2141898748; constexpr int kIntentNotFound = -2141898746; constexpr int kValueNotFound = -2141898745; + constexpr int kLauncherInvalidArgument = -2141898236; + constexpr int kLauncherNotInitialized = -2141898238; Require(Throws([] { sceNpGameIntentInitParamInit(nullptr); }), "null param init did not throw"); @@ -41,27 +50,115 @@ 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(sceNpAppLauncherLaunchApp(0, "app", nullptr) == kLauncherNotInitialized, + "launch before init must report not-initialized"); Require(sceNpAppLauncherInitialize(appLauncherParam) == 0, "app launcher init failed"); - Require(Throws([] { sceNpAppLauncherLaunchApp(); }), - "app launcher launch must throw until implemented"); + Require(sceNpAppLauncherLaunchApp(0, nullptr, nullptr) == kLauncherInvalidArgument, + "null launch argument must be invalid"); + Require(sceNpAppLauncherLaunchApp(0, "", nullptr) == kLauncherInvalidArgument, + "empty launch argument must be invalid"); + std::vector unterminated(0x2001, 'x'); + Require(sceNpAppLauncherLaunchApp(0, "app", unterminated.data()) == kLauncherInvalidArgument, + "non-terminated 0x2001-byte argument must be invalid"); Require(sceNpAppLauncherTerminate() == 0, "app launcher terminate failed"); + Require(sceNpAppLauncherLaunchApp(0, "app", nullptr) == kLauncherNotInitialized, + "launch after terminate must report not-initialized"); *reinterpret_cast(appLauncherParam) = 0x10; Require(sceNpAppLauncherInitialize(appLauncherParam) == 0, "app launcher init must accept any init parameter"); + std::atomic_bool stop{false}; + std::thread writer([&] { + while (!stop.load(std::memory_order_relaxed)) { + sceNpAppLauncherInitialize(nullptr); + sceNpAppLauncherTerminate(); + } + }); + std::thread reader([&] { + while (!stop.load(std::memory_order_relaxed)) { + try { + (void)sceNpAppLauncherLaunchApp(0, "app", nullptr); + } catch (const std::runtime_error&) { + } + } + }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + stop.store(true); + writer.join(); + reader.join(); + + Require(sceNpAppLauncherTerminate() == 0, "final terminate failed"); + Require(sceNpAppLauncherLaunchApp(0, "app", nullptr) == kLauncherNotInitialized, + "final launch must report not-initialized"); + std::puts("NpGameIntent tests passed"); return 0; } diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index 43c844bb62..1674bdfecc 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,181 @@ 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); +} + +struct SentinelBuffer { + unsigned char bytes[64]; + SentinelBuffer() { std::memset(bytes, 0x5a, sizeof(bytes)); } + bool Untouched() const { + for (const unsigned char byte : bytes) { + if (byte != 0x5a) return false; + } + return true; + } +}; + +void CheckManagerIntProfileGetters() { + const int live = sceNpManagerIntCreateRequest(); + const int deleted = sceNpManagerIntCreateRequest(); + Require(live > 0 && deleted > 0 && sceNpManagerIntDeleteRequest(deleted) == 0, "NpManagerInt requests for the profile getters"); + SentinelBuffer out; + Require(sceNpManagerIntGetAccountAge(live, -1, out.bytes) == InvalidArgument && out.Untouched(), "sceNpManagerIntGetAccountAge must refuse user id -1"); + Require(sceNpManagerIntGetAccountAge(live, InitialUserId, nullptr) == InvalidArgument, "sceNpManagerIntGetAccountAge must refuse a null output"); + Require(sceNpManagerIntGetAccountAge(deleted, InitialUserId, nullptr) == InvalidArgument, "sceNpManagerIntGetAccountAge checks its arguments before the request"); + Require(sceNpManagerIntGetAccountLanguageA(live, -1, out.bytes) == InvalidArgument && out.Untouched(), "sceNpManagerIntGetAccountLanguageA must refuse user id -1"); + Require(sceNpManagerIntGetAccountLanguageA(live, InitialUserId, nullptr) == InvalidArgument, "sceNpManagerIntGetAccountLanguageA must refuse a null output"); + Require(sceNpManagerIntGetAccountLanguageA(deleted, InitialUserId, nullptr) == InvalidArgument, "sceNpManagerIntGetAccountLanguageA checks its arguments before the request"); + for (const int request : {live, deleted, 0x7fffffff, 0, -1}) { + Require(sceNpManagerIntGetAccountAge(request, InitialUserId, out.bytes) == SignedOut && out.Untouched(), "sceNpManagerIntGetAccountAge must report NP as signed out and write nothing"); + Require(sceNpManagerIntGetAccountLanguageA(request, InitialUserId, out.bytes) == SignedOut && out.Untouched(), "sceNpManagerIntGetAccountLanguageA must report NP as signed out and write nothing"); + } + Require(sceNpManagerIntGetAccountCountryA(-1, out.bytes) == InvalidArgument && out.Untouched(), "sceNpManagerIntGetAccountCountryA must refuse user id -1"); + Require(sceNpManagerIntGetAccountCountryA(InitialUserId, nullptr) == InvalidArgument, "sceNpManagerIntGetAccountCountryA must refuse a null output"); + Require(sceNpManagerIntGetAccountCountryA(InitialUserId, out.bytes) == SignedOut && out.Untouched(), "sceNpManagerIntGetAccountCountryA must report NP as signed out and write nothing"); + NpId* npId = reinterpret_cast(out.bytes); + Require(sceNpManagerIntGetNpId(-1, npId) == InvalidArgument && out.Untouched(), "sceNpManagerIntGetNpId must refuse user id -1"); + Require(sceNpManagerIntGetNpId(InitialUserId, nullptr) == InvalidArgument, "sceNpManagerIntGetNpId must refuse a null output"); + Require(sceNpManagerIntGetNpId(InitialUserId, npId) == SignedOut && out.Untouched(), "sceNpManagerIntGetNpId must report NP as signed out and write nothing"); + NpOnlineId* onlineId = reinterpret_cast(out.bytes); + Require(sceNpManagerIntGetOnlineId(-1, onlineId) == InvalidArgument && out.Untouched(), "sceNpManagerIntGetOnlineId must refuse user id -1"); + Require(sceNpManagerIntGetOnlineId(InitialUserId, nullptr) == InvalidArgument, "sceNpManagerIntGetOnlineId must refuse a null output"); + Require(sceNpManagerIntGetOnlineId(InitialUserId, onlineId) == SignedOut && out.Untouched(), "sceNpManagerIntGetOnlineId must report NP as signed out and write nothing"); + Require(sceNpManagerIntDeleteRequest(live) == 0, "the profile getters leave a live request live"); + Require(sceNpManagerIntDeleteRequest(deleted) == ManagerIntNotInTable, "the profile getters leave a deleted request deleted"); +} + +void CheckManagerIntUserLookups() { + NpOnlineId onlineId{}; + std::memcpy(onlineId.data, "player", 6); + NpId npId{}; + npId.online_id = onlineId; + int userId = 0x5a5a5a5a; + Require(sceNpManagerIntGetUserIdByOnlineId(nullptr, &userId) == InvalidArgument && userId == 0x5a5a5a5a, "sceNpManagerIntGetUserIdByOnlineId must refuse a null online id"); + Require(sceNpManagerIntGetUserIdByOnlineId(&onlineId, nullptr) == InvalidArgument, "sceNpManagerIntGetUserIdByOnlineId must refuse a null output"); + Require(sceNpManagerIntGetUserIdByOnlineId(&onlineId, &userId) == SignedOut && userId == 0x5a5a5a5a, "sceNpManagerIntGetUserIdByOnlineId must report NP as signed out and write nothing"); + Require(sceNpManagerIntGetUserIdByNpId(nullptr, &userId) == InvalidArgument && userId == 0x5a5a5a5a, "sceNpManagerIntGetUserIdByNpId must refuse a null NpId"); + Require(sceNpManagerIntGetUserIdByNpId(&npId, nullptr) == InvalidArgument, "sceNpManagerIntGetUserIdByNpId must refuse a null output"); + Require(sceNpManagerIntGetUserIdByNpId(&npId, &userId) == SignedOut && userId == 0x5a5a5a5a, "sceNpManagerIntGetUserIdByNpId must report NP as signed out and write nothing"); +} + +void CheckManagerIntAuthorization() { + constexpr int InvalidPlatformType = static_cast(0x80550004u); + constexpr int OutOfMemory = static_cast(0x80550005u); + const int live = sceNpManagerIntCreateRequest(); + Require(live > 0, "an NpManagerInt request for the authorization calls"); + SentinelBuffer parameter; + SentinelBuffer code; + SentinelBuffer extra; + auto untouched = [&] { return parameter.Untouched() && code.Untouched() && extra.Untouched(); }; + Require(sceNpManagerIntAuthGetAuthorizationCode(live, -1, parameter.bytes, nullptr, 0, 1, 0, code.bytes) == InvalidArgument && untouched(), "sceNpManagerIntAuthGetAuthorizationCode must refuse user id -1"); + Require(sceNpManagerIntAuthGetAuthorizationCode(live, InitialUserId, nullptr, nullptr, 0, 1, 0, code.bytes) == InvalidArgument && untouched(), "sceNpManagerIntAuthGetAuthorizationCode must refuse a null parameter"); + Require(sceNpManagerIntAuthGetAuthorizationCode(live, InitialUserId, parameter.bytes, nullptr, 0, 1, 0, nullptr) == InvalidArgument && untouched(), "sceNpManagerIntAuthGetAuthorizationCode must refuse a null output"); + Require(sceNpManagerIntAuthGetAuthorizationCode(live, InitialUserId, parameter.bytes, nullptr, 4, 1, 0, code.bytes) == InvalidArgument && untouched(), "sceNpManagerIntAuthGetAuthorizationCode must refuse a first value above 3"); + Require(sceNpManagerIntAuthGetAuthorizationCode(live, InitialUserId, parameter.bytes, nullptr, 0, 0, 0, code.bytes) == InvalidArgument && untouched(), "sceNpManagerIntAuthGetAuthorizationCode must refuse a second value of 0"); + Require(sceNpManagerIntAuthGetAuthorizationCode(live, InitialUserId, parameter.bytes, nullptr, 0, 4, 0, code.bytes) == InvalidArgument && untouched(), "sceNpManagerIntAuthGetAuthorizationCode must refuse a second value above 3"); + Require(sceNpManagerIntAuthGetAuthorizationCode(live, InitialUserId, parameter.bytes, nullptr, 0, 1, 0, code.bytes) == SignedOut && untouched(), "sceNpManagerIntAuthGetAuthorizationCode must report NP as signed out and write nothing"); + Require(sceNpManagerIntAuthGetAuthorizationCode(0x7fffffff, InitialUserId, parameter.bytes, extra.bytes, 3, 3, 0xff, code.bytes) == SignedOut && untouched(), "sceNpManagerIntAuthGetAuthorizationCode with the optional pointer and the upper values"); + Require(sceNpManagerIntAuthGetIdToken(live, -1, parameter.bytes, parameter.bytes, parameter.bytes, 0, code.bytes) == InvalidArgument && untouched(), "sceNpManagerIntAuthGetIdToken must refuse user id -1"); + Require(sceNpManagerIntAuthGetIdToken(live, InitialUserId, nullptr, parameter.bytes, parameter.bytes, 0, code.bytes) == InvalidArgument && untouched(), "sceNpManagerIntAuthGetIdToken must refuse a null first pointer"); + Require(sceNpManagerIntAuthGetIdToken(live, InitialUserId, parameter.bytes, nullptr, parameter.bytes, 0, code.bytes) == InvalidArgument && untouched(), "sceNpManagerIntAuthGetIdToken must refuse a null second pointer"); + Require(sceNpManagerIntAuthGetIdToken(live, InitialUserId, parameter.bytes, parameter.bytes, nullptr, 0, code.bytes) == InvalidArgument && untouched(), "sceNpManagerIntAuthGetIdToken must refuse a null third pointer"); + Require(sceNpManagerIntAuthGetIdToken(live, InitialUserId, parameter.bytes, parameter.bytes, parameter.bytes, 0, nullptr) == InvalidArgument && untouched(), "sceNpManagerIntAuthGetIdToken must refuse a null output"); + Require(sceNpManagerIntAuthGetIdToken(live, InitialUserId, parameter.bytes, parameter.bytes, parameter.bytes, ~std::uint64_t{0}, code.bytes) == SignedOut && untouched(), "sceNpManagerIntAuthGetIdToken must report NP as signed out and write nothing"); + Require(sceNpManagerIntGetAuthorizationCode(live, -1, parameter.bytes, parameter.bytes, nullptr, 0, 0, nullptr, code.bytes) == InvalidArgument && untouched(), "sceNpManagerIntGetAuthorizationCode must refuse user id -1"); + Require(sceNpManagerIntGetAuthorizationCode(live, InitialUserId, nullptr, parameter.bytes, nullptr, 0, 0, nullptr, code.bytes) == InvalidArgument && untouched(), "sceNpManagerIntGetAuthorizationCode must refuse a null first pointer"); + Require(sceNpManagerIntGetAuthorizationCode(live, InitialUserId, parameter.bytes, nullptr, nullptr, 0, 0, nullptr, code.bytes) == InvalidArgument && untouched(), "sceNpManagerIntGetAuthorizationCode must refuse a null second pointer"); + Require(sceNpManagerIntGetAuthorizationCode(live, InitialUserId, parameter.bytes, parameter.bytes, nullptr, 0, 0, nullptr, nullptr) == InvalidArgument && untouched(), "sceNpManagerIntGetAuthorizationCode must refuse a null output"); + for (const std::uint32_t mode : {1u, 2u, 4u, 6u, 0xffffffffu}) { + Require(sceNpManagerIntGetAuthorizationCode(live, InitialUserId, parameter.bytes, parameter.bytes, nullptr, mode, 0, nullptr, code.bytes) == InvalidPlatformType && untouched(), "sceNpManagerIntGetAuthorizationCode must refuse a mode other than 0, 3 or 5"); + } + Require(sceNpManagerIntGetAuthorizationCode(live, -1, parameter.bytes, parameter.bytes, nullptr, 1, 0, nullptr, code.bytes) == InvalidArgument && untouched(), "sceNpManagerIntGetAuthorizationCode checks the user id before the mode"); + for (const std::uint32_t mode : {0u, 3u, 5u}) { + Require(sceNpManagerIntGetAuthorizationCode(live, InitialUserId, parameter.bytes, parameter.bytes, extra.bytes, mode, 0xff, extra.bytes, code.bytes) == SignedOut && untouched(), "sceNpManagerIntGetAuthorizationCode must report NP as signed out and write nothing"); + } + static char longUri[0x2001]; + std::memset(longUri, 'a', 0x2000); + const char* uri = "https://example.com/"; + Require(sceNpManagerIntGetAuthorizationCodeWithPsnoUri(live, -1, uri, 0, code.bytes, 0x801) == InvalidArgument && untouched(), "sceNpManagerIntGetAuthorizationCodeWithPsnoUri must refuse user id -1"); + Require(sceNpManagerIntGetAuthorizationCodeWithPsnoUri(live, InitialUserId, nullptr, 0, code.bytes, 0x801) == InvalidArgument && untouched(), "sceNpManagerIntGetAuthorizationCodeWithPsnoUri must refuse a null URI"); + Require(sceNpManagerIntGetAuthorizationCodeWithPsnoUri(live, InitialUserId, uri, 0, nullptr, 0x801) == InvalidArgument && untouched(), "sceNpManagerIntGetAuthorizationCodeWithPsnoUri must refuse a null output"); + Require(sceNpManagerIntGetAuthorizationCodeWithPsnoUri(live, InitialUserId, uri, 0, code.bytes, 0) == InvalidSize && untouched(), "sceNpManagerIntGetAuthorizationCodeWithPsnoUri must refuse a size of 0"); + Require(sceNpManagerIntGetAuthorizationCodeWithPsnoUri(live, InitialUserId, uri, 0, code.bytes, 0x802) == InvalidSize && untouched(), "sceNpManagerIntGetAuthorizationCodeWithPsnoUri must refuse a size above 0x801"); + Require(sceNpManagerIntGetAuthorizationCodeWithPsnoUri(live, InitialUserId, uri, 4, code.bytes, 0x801) == InvalidPlatformType && untouched(), "sceNpManagerIntGetAuthorizationCodeWithPsnoUri must refuse a mode other than 0, 3 or 5"); + Require(sceNpManagerIntGetAuthorizationCodeWithPsnoUri(live, InitialUserId, longUri, 0, code.bytes, 0x801) == OutOfMemory && untouched(), "sceNpManagerIntGetAuthorizationCodeWithPsnoUri must refuse a URI of 0x2000 bytes"); + Require(sceNpManagerIntGetAuthorizationCodeWithPsnoUri(live, InitialUserId, nullptr, 0, code.bytes, 0) == InvalidArgument && untouched(), "sceNpManagerIntGetAuthorizationCodeWithPsnoUri checks a null URI before the size"); + Require(sceNpManagerIntGetAuthorizationCodeWithPsnoUri(live, InitialUserId, uri, 4, code.bytes, 0) == InvalidSize && untouched(), "sceNpManagerIntGetAuthorizationCodeWithPsnoUri checks the size before the mode"); + Require(sceNpManagerIntGetAuthorizationCodeWithPsnoUri(live, InitialUserId, longUri, 4, code.bytes, 0x801) == InvalidPlatformType && untouched(), "sceNpManagerIntGetAuthorizationCodeWithPsnoUri checks the mode before the URI length"); + for (const std::size_t size : {std::size_t{1}, std::size_t{0x801}}) { + Require(sceNpManagerIntGetAuthorizationCodeWithPsnoUri(live, InitialUserId, uri, 5, code.bytes, size) == SignedOut && untouched(), "sceNpManagerIntGetAuthorizationCodeWithPsnoUri must report NP as signed out and write nothing"); + } + Require(sceNpManagerIntDeleteRequest(live) == 0, "the authorization calls leave a live request live"); +} + void CheckOnlineIdQueries() { NpOnlineId onlineId{}; std::memcpy(onlineId.data, "player", 6); @@ -374,6 +558,13 @@ int main() { CheckLegacyStateCallback(); CheckRequestTimeouts(); CheckAsyncRequestLifecycle(); + CheckIdMapperRequests(); + CheckManagerIntRequests(); + CheckIdMapperLookup("sceNpIdMapperNpIdToAccountId", sceNpIdMapperNpIdToAccountId); + CheckIdMapperLookup("sceNpIdMapperOnlineIdToAccountId", sceNpIdMapperOnlineIdToAccountId); + CheckManagerIntProfileGetters(); + CheckManagerIntUserLookups(); + CheckManagerIntAuthorization(); 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/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/GuestNpTrophy2.cpp b/core/libs/tests/GuestNpTrophy2.cpp index 2dcaa5f409..3468d57230 100644 --- a/core/libs/tests/GuestNpTrophy2.cpp +++ b/core/libs/tests/GuestNpTrophy2.cpp @@ -29,6 +29,9 @@ int main() { Require(sceNpTrophy2ShowTrophyList(1) == 0); + auto* volatile linked = &sceNpTrophy2SystemCreateContext; + Require(linked != nullptr); + NpTrophy2GameDetails details{}; NpTrophy2GameData data{}; Require(sceNpTrophy2GetGameInfo(1, 1, &details, nullptr) == 0); diff --git a/core/libs/tests/GuestNpTrophyData.cpp b/core/libs/tests/GuestNpTrophyData.cpp index 6810f8a3da..71275707f9 100644 --- a/core/libs/tests/GuestNpTrophyData.cpp +++ b/core/libs/tests/GuestNpTrophyData.cpp @@ -223,6 +223,11 @@ static void ReportPlaceholders() { } } +static void ReportExportLinked() { + auto* volatile linked = &sceNpUniversalDataSystemIntInitialize; + Require(linked != nullptr, "sceNpUniversalDataSystemIntInitialize is not linked"); +} + int main() { const auto previous = std::filesystem::current_path(); const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-data-" + std::to_string(std::random_device{}())); @@ -236,6 +241,7 @@ int main() { ReportTrophyData(context); PostUnlocks(context); ReportPlaceholders(); + ReportExportLinked(); std::filesystem::current_path(previous); std::filesystem::remove_all(root); 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/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 beab5d7f6a..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); @@ -633,6 +669,8 @@ int main() { CheckReadQueue(handle); CheckTapBetweenReads(); CheckLatchedPressLifetime(handle); + CheckLatchedPressAtSlowReads(handle); + CheckLatchedPressPerPort(); CheckReadStateHandle(handle); CheckRemoteController(handle); CheckTrackerDevices(handle); 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 e06614a544..529e3fd923 100644 --- a/core/libs/tests/GuestPlayGo.cpp +++ b/core/libs/tests/GuestPlayGo.cpp @@ -287,6 +287,22 @@ 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", "{}"}}); 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 c7d86b21db..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); @@ -286,6 +307,7 @@ static void PollsAHungUpSocketWithoutPollout() { #endif int main() { + for (const int descriptor : {0, 1, 2}) StandardDescriptorFile(descriptor); PollsUrgentData(); PollsAStreamWhosePeerClosed(); #ifndef _WIN32 diff --git a/core/libs/tests/GuestPosixScalarIo.cpp b/core/libs/tests/GuestPosixScalarIo.cpp index ea79971ed5..107c110062 100644 --- a/core/libs/tests/GuestPosixScalarIo.cpp +++ b/core/libs/tests/GuestPosixScalarIo.cpp @@ -180,6 +180,20 @@ int main() { Require(_read_nid_postfix(capacityDescriptors[0], largeBuffer.data(), largeBuffer.size()) == static_cast(largeBuffer.size())); Require(largeBuffer == largePayload); Require(close_nid_postfix(capacityDescriptors[0]) == 0); +#ifdef _WIN32 + constexpr int GuestEpipe = 32; + int brokenPipe[2] = {-1, -1}; + Require(pipe_nid_postfix(brokenPipe) == 0); + Require(close_nid_postfix(brokenPipe[0]) == 0); + RequirePosixFailure([&] { return write_nid_postfix(brokenPipe[1], pipePayload, 1); }, GuestEpipe); + RequirePosixFailure([&] { return _write_nid_postfix(brokenPipe[1], pipePayload, 1); }, GuestEpipe); + Require(sceKernelWrite(brokenPipe[1], pipePayload, 1) == static_cast(0x80020000u | GuestEpipe)); + char brokenByte = 'b'; + const KernelIovec brokenVector{&brokenByte, 1}; + RequirePosixFailure([&] { return writev_nid_postfix(brokenPipe[1], &brokenVector, 1); }, GuestEpipe); + Require(sceKernelWritev(brokenPipe[1], &brokenVector, 1) == static_cast(0x80020000u | GuestEpipe)); + Require(close_nid_postfix(brokenPipe[1]) == 0); +#endif #ifndef _WIN32 int nonBlockingPipe[2] = {-1, -1}; Require(pipe_nid_postfix(nonBlockingPipe) == 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 d2cc7925b7..f0ec1a6ace 100644 --- a/core/libs/tests/GuestPsml.cpp +++ b/core/libs/tests/GuestPsml.cpp @@ -1,11 +1,10 @@ #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,87 +66,27 @@ void SetupBeforeInit() { Require(scePsmlMfsrIsCaptureInProgress(sentinel) == kErrNotInitialized); } -void SetupAfterInit() { - Require(scePsmlMfsrInit() == 0); - Require(scePsmlMfsrInit() == 0); +void Mfsr1OnBaseConsole() { + constexpr std::int32_t unsupportedConsole = static_cast(0x8A810012); + Require(scePsmlMfsrInit() == unsupportedConsole); + Require(scePsmlMfsrInit() == unsupportedConsole); + SetupBeforeInit(); +} - 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 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); } void Mfsr2OnBaseConsole() { @@ -223,43 +127,6 @@ void Mfsr2OnBaseConsole() { Require(dwords == untouchedDwords); } -void RefusedAfterInit() { - MfsrSharedResourcesParam sharedParam{}; - MfsrRequirement requirement{}; - sharedParam.type = 1; - Require(Throws([&] { scePsmlMfsrGetSharedResourcesInitRequirement(&requirement, &sharedParam); }, "type 1 is not supported")); - - 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); - - 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 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")); -} - } int main() { @@ -304,7 +171,7 @@ int main() { DispatchPacketUntouched(scePsmlMfsrGetDispatchMfsrPacket1000); DispatchPacketUntouched(scePsmlMfsrGetDispatchMfsrPacket1300); SetupBeforeInit(); + Mfsr1OnBaseConsole(); Mfsr2OnBaseConsole(); - SetupAfterInit(); - RefusedAfterInit(); + ConcurrentCreatesUninitialized(); } diff --git a/core/libs/tests/GuestPthreadAttr.cpp b/core/libs/tests/GuestPthreadAttr.cpp index 7e85c80984..e15870361b 100644 --- a/core/libs/tests/GuestPthreadAttr.cpp +++ b/core/libs/tests/GuestPthreadAttr.cpp @@ -196,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); @@ -218,10 +220,15 @@ 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(); 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 465bd7c678..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,17 +54,33 @@ int main() { PthreadCondattr attr = nullptr; Require(scePthreadCondattrInit(&attr) == SCE_OK); + 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); @@ -58,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/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/GuestPthreadSecondJoin.cpp b/core/libs/tests/GuestPthreadSecondJoin.cpp index 3bca2b8745..f8e44c032e 100644 --- a/core/libs/tests/GuestPthreadSecondJoin.cpp +++ b/core/libs/tests/GuestPthreadSecondJoin.cpp @@ -62,9 +62,11 @@ int main() { Require(scePthreadCreate(&named, nullptr, Finish, nullptr, "0123456789abcdefghijklmnopqrstu") == SCE_OK); Require(scePthreadGetname(named, name) == SCE_OK && std::strcmp(name, "0123456789abcdefghijklmnopqrstu") == 0); Require(scePthreadJoin(named, nullptr) == SCE_OK); - Pthread untouched = reinterpret_cast(std::uintptr_t{0x77}); - Require(scePthreadCreate(&untouched, nullptr, Finish, nullptr, "0123456789abcdefghijklmnopqrstuv") == SCE_KERNEL_ERROR_ENAMETOOLONG); - Require(untouched == reinterpret_cast(std::uintptr_t{0x77})); + for (const char* tooLong : {"0123456789abcdefghijklmnopqrstuv", "0123456789abcdefghijklmnopqrstuvw"}) { + Pthread cleared = reinterpret_cast(std::uintptr_t{0x77}); + Require(scePthreadCreate(&cleared, nullptr, Finish, nullptr, tooLong) == SCE_KERNEL_ERROR_ENAMETOOLONG); + Require(cleared == nullptr); + } Pthread unnamed = nullptr; std::memset(name, 'x', sizeof(name)); Require(scePthreadCreate(&unnamed, nullptr, Finish, nullptr, nullptr) == SCE_OK); diff --git a/core/libs/tests/GuestPthreadSetname.cpp b/core/libs/tests/GuestPthreadSetname.cpp new file mode 100644 index 0000000000..3b57956564 --- /dev/null +++ b/core/libs/tests/GuestPthreadSetname.cpp @@ -0,0 +1,67 @@ +#include "prx/libkernel/include/Api.hpp" +#include "SceTypes.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" +#include +#include +#include +#include + +static constexpr int GuestEagain = 35; + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "pthread setname 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 TestMutex() { + PthreadMutex mutex = nullptr; + Require(pthread_mutex_init_nid_postfix(&mutex, nullptr) == 0); + Require(pthread_mutex_setname_np_nid_postfix(&mutex, "first") == 0 && mutex->_name == "first"); + Require(pthread_mutex_setname_np_nid_postfix(&mutex, "second") == GuestEagain && mutex->_name == "first"); + Require(pthread_mutex_lock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_setname_np_nid_postfix(&mutex, "held") == GuestEagain && mutex->_name == "first"); + Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); + RequireThrows([] { pthread_mutex_setname_np_nid_postfix(nullptr, "mutex"); }); + Require(pthread_mutex_destroy_nid_postfix(&mutex) == 0); + RequireThrows([&] { pthread_mutex_setname_np_nid_postfix(&mutex, "destroyed"); }); +} + +static void TestRwlock() { + PthreadRwlock rwlock = nullptr; + Require(pthread_rwlock_init_nid_postfix(&rwlock, nullptr) == 0); + Require(pthread_rwlock_setname_np_nid_postfix(&rwlock, "first") == 0 && rwlock->_name == "first"); + Require(pthread_rwlock_rdlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_setname_np_nid_postfix(&rwlock, "read") == GuestEagain && rwlock->_name == "first"); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + RequireThrows([] { pthread_rwlock_setname_np_nid_postfix(nullptr, "rwlock"); }); + Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); + RequireThrows([&] { pthread_rwlock_setname_np_nid_postfix(&rwlock, "destroyed"); }); + PthreadRwlock unset = nullptr; + RequireThrows([&] { pthread_rwlock_setname_np_nid_postfix(&unset, "static"); }); +} + +static void TestBarrier() { + PthreadBarrierPrivate* barrier = nullptr; + Require(pthread_barrier_init_nid_postfix(&barrier, nullptr, 1) == 0); + Require(pthread_barrier_setname_np_nid_postfix(&barrier, "first") == 0 && barrier->name == "first"); + Require(pthread_barrier_wait_nid_postfix(&barrier) == -1); + Require(pthread_barrier_setname_np_nid_postfix(&barrier, "second") == GuestEagain && barrier->name == "first"); + RequireThrows([] { pthread_barrier_setname_np_nid_postfix(nullptr, "barrier"); }); + Require(pthread_barrier_destroy_nid_postfix(&barrier) == 0); + RequireThrows([&] { pthread_barrier_setname_np_nid_postfix(&barrier, "destroyed"); }); + PthreadBarrierPrivate* unset = nullptr; + RequireThrows([&] { pthread_barrier_setname_np_nid_postfix(&unset, "unset"); }); +} + +int main() { + TestMutex(); + TestRwlock(); + TestBarrier(); + std::puts("pthread setname tests passed"); +} diff --git a/core/libs/tests/GuestRaiseExcept.cpp b/core/libs/tests/GuestRaiseExcept.cpp new file mode 100644 index 0000000000..446a772297 --- /dev/null +++ b/core/libs/tests/GuestRaiseExcept.cpp @@ -0,0 +1,173 @@ +#include "prx/libc/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 +#endif + +using Raise = int (APS5_VABI*)(int); +Raise raiseFlags; + +void Require(bool condition, const char* message) { + if (!condition) { std::fprintf(stderr,"feraiseexcept: %s\n",message); std::abort(); } +} + +struct Environment { std::array x87; std::uint32_t sse; }; +Environment Read() { + Environment state; + asm volatile("fnstenv %0\n\tfldenv %0" : "=m"(state.x87)); + state.sse=_mm_getcsr(); + return state; +} +void Write(const Environment& state) { + asm volatile("fldenv %0" : : "m"(state.x87)); + _mm_setcsr(state.sse); +} + +void Flags() { + const auto original=Read(); + for (const unsigned precision: {0u,0x200u,0x300u}) + for (unsigned rounding=0;rounding<16;++rounding) + for (unsigned old=0;old<64;++old) + for (unsigned raised=0;raised<64;++raised) { + auto before=original; + before.x87[0]=(before.x87[0]&~0xf3fu)|0x3f|precision|((rounding&3)<<10); + before.x87[1]=(before.x87[1]&~0xffu)|old; + before.sse=(before.sse&~0x603fu)|(0x3f<<7)|((rounding>>2)<<13)|(old^0x3f); + Write(before); + before=Read(); + *__error_nid_postfix()=123; + Require(raiseFlags(static_cast(raised))==0,"return value"); + const auto after=Read(); + Require(*__error_nid_postfix()==123,"errno preserved"); + Require((after.x87[1]&0x3f)==(old|raised),"x87 flags accumulate"); + Require((after.sse&0x3f)==((old^0x3f)|raised),"SSE flags accumulate independently"); + Require((after.x87[1]&~0x3fu)==(before.x87[1]&~0x3fu),"other x87 status preserved"); + Require((after.sse&~0x3fu)==(before.sse&~0x3fu),"SSE controls preserved"); + for (unsigned word: {0u,2u,3u,4u,5u,6u}) + Require(after.x87[word]==before.x87[word],"x87 controls, tags and pointers preserved"); + } + Write(original); + for (const int invalid: {-1,0x40,0x10000,std::numeric_limits::min(),std::numeric_limits::max()}) { + bool rejected=false; + try { raiseFlags(invalid); } catch (const std::exception&) { rejected=true; } + Require(rejected,"unverified exception bits rejected"); + const auto after=Read(); + Require(after.x87==original.x87 && after.sse==original.sse,"rejected mask preserves environment"); + } +} + +void DifferentMasks() { + const auto original=Read(); + for (unsigned bit=0;bit<6;++bit) { + auto seed=original; + seed.x87[0]|=0x3f; + seed.x87[1]&=~0xffu; + seed.sse=(seed.sse&~0x3fu)|(0x3f<<7); + seed.sse&=~(1u<<(bit+7)); + Write(seed); + const int result=raiseFlags(static_cast(1u<(100+flag); + Require(raiseFlags(static_cast(flag))==0,"thread return"); + const int raisedError=*__error_nid_postfix(); + ready.arrive_and_wait(); + const auto after=Read(); + Require((after.x87[1]&0x3f)==flag && (after.sse&0x3f)==flag,"thread-local exception flags"); + Require(raisedError==static_cast(100+flag),"thread-local errno"); + Write(original); + }; + std::thread a(worker,1),b(worker,4); + a.join(); b.join(); +} + +#ifdef _WIN32 +DWORD expectedException; +unsigned expectedFlag; +LONG CALLBACK Trap(EXCEPTION_POINTERS* exception) { + const auto code=exception->ExceptionRecord->ExceptionCode; + const auto& state=exception->ContextRecord->FltSave; + const auto active=state.StatusWord & ~state.ControlWord & 0x3f; + const bool denormalCode=expectedFlag==2 && code==EXCEPTION_FLT_INVALID_OPERATION; + if ((code==expectedException || denormalCode) && active==expectedFlag) std::_Exit(0); + return EXCEPTION_CONTINUE_SEARCH; +} +#else +void Trap(int) { std::_Exit(0); } +#endif + +void CheckTrap(unsigned bit) { + Require(bit<6,"trap bit"); +#ifdef _WIN32 + constexpr DWORD codes[]{EXCEPTION_FLT_INVALID_OPERATION,EXCEPTION_FLT_DENORMAL_OPERAND,EXCEPTION_FLT_DIVIDE_BY_ZERO,EXCEPTION_FLT_OVERFLOW,EXCEPTION_FLT_UNDERFLOW,EXCEPTION_FLT_INEXACT_RESULT}; + expectedException=codes[bit]; + expectedFlag=1u<(1u<(GetProcAddress(library,"feraiseexcept_nid_postfix")); +#else + const auto library=dlopen(argv[1],RTLD_NOW); + Require(library!=nullptr,"load libc"); + raiseFlags=reinterpret_cast(dlsym(library,"feraiseexcept_nid_postfix")); +#endif + Require(raiseFlags!=nullptr,"feraiseexcept export is missing"); + if (argc==3) CheckTrap(static_cast(std::atoi(argv[2]))); + Flags(); DifferentMasks(); Threads(); +#ifdef _WIN32 + Require(FreeLibrary(library)!=0,"release lookup handle"); +#else + Require(dlclose(library)==0,"release lookup handle"); +#endif + std::puts("feraiseexcept checks passed"); +} 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 6e126f762d..5c5cf1e570 100644 --- a/core/libs/tests/GuestRandomState.cpp +++ b/core/libs/tests/GuestRandomState.cpp @@ -180,11 +180,22 @@ int main() { initstate_nid_postfix(7, small.data(), static_cast(small.size())); random_nid_postfix(); - alignas(4) std::array garbled{}; + alignas(4) std::array garbled{}; const std::uint32_t garbledWord = 5 * 7 + 1; std::memcpy(garbled.data(), &garbledWord, sizeof(garbledWord)); - Require(setstate_nid_postfix(garbled.data()) == nullptr, "setstate must reject a rear position past the degree"); - Require(random_nid_postfix() == seed7Type1[1], "a rejected setstate must keep the current state"); + Require(setstate_nid_postfix(garbled.data()) == small.data(), "setstate must take a rear position past the degree as the console does"); + random_nid_postfix(); + Require(setstate_nid_postfix(small.data()) == garbled.data(), "setstate back from the garbled state"); + Require(random_nid_postfix() == seed7Type1[1], "returning to the earlier state must resume its sequence"); + + initstate_nid_postfix(7, small.data(), static_cast(small.size())); + for (int draw = 0; draw < 3; ++draw) random_nid_postfix(); + alignas(4) std::array refused{}; + Require(initstate_nid_postfix(1, refused.data(), 4) == nullptr, "initstate below 8 bytes returns null"); + Require(Word(small.data(), 0) == 3 * 5 + 1, "initstate below 8 bytes still saves the current state's rear position"); + initstate_nid_postfix(7, small.data(), static_cast(small.size())); + random_nid_postfix(); + random_nid_postfix(); *__error_nid_postfix() = 1234; std::array tiny{}; 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 8d9b302b49..830f09c5d9 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -186,59 +186,26 @@ 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); - Require(sceRtcSetYear(&converted, 0) == invalidYear); - Require(sceRtcSetYear(&converted, 10000) == invalidYear); - Require(sceRtcSetYear(&converted, 1) == 0 && sceRtcGetYear(&converted) == 1); - Require(sceRtcSetYear(&converted, 9999) == 0 && sceRtcGetYear(&converted) == 9999); - Require(sceRtcSetYear(&converted, 2026) == 0 && sceRtcGetYear(&converted) == 2026); - - Require(sceRtcGetMonth(nullptr) == invalidPointer); - Require(sceRtcSetMonth(nullptr, 6) == invalidPointer); - Require(sceRtcSetMonth(&converted, 0) == invalidMonth); - Require(sceRtcSetMonth(&converted, 13) == invalidMonth); - Require(sceRtcSetMonth(&converted, 1) == 0 && sceRtcGetMonth(&converted) == 1); - Require(sceRtcSetMonth(&converted, 12) == 0 && sceRtcGetMonth(&converted) == 12); - - Require(sceRtcGetDay(nullptr) == invalidPointer); - Require(sceRtcSetDay(nullptr, 15) == invalidPointer); - Require(sceRtcSetDay(&converted, 0) == invalidDay); - Require(sceRtcSetDay(&converted, 32) == invalidDay); - Require(sceRtcSetDay(&converted, 1) == 0 && sceRtcGetDay(&converted) == 1); - Require(sceRtcSetDay(&converted, 31) == 0 && sceRtcGetDay(&converted) == 31); - - Require(sceRtcGetHour(nullptr) == invalidPointer); - Require(sceRtcSetHour(nullptr, 12) == invalidPointer); - Require(sceRtcSetHour(&converted, -1) == invalidHour); - Require(sceRtcSetHour(&converted, 24) == invalidHour); - Require(sceRtcSetHour(&converted, 0) == 0 && sceRtcGetHour(&converted) == 0); - Require(sceRtcSetHour(&converted, 23) == 0 && sceRtcGetHour(&converted) == 23); - - constexpr int invalidMinute = static_cast(0x80B5000C); - constexpr int invalidSecond = static_cast(0x80B5000D); - - Require(sceRtcGetMinute(nullptr) == invalidPointer); - Require(sceRtcSetMinute(nullptr, 30) == invalidPointer); - Require(sceRtcSetMinute(&converted, -1) == invalidMinute); - Require(sceRtcSetMinute(&converted, 60) == invalidMinute); - Require(sceRtcSetMinute(&converted, 0) == 0 && sceRtcGetMinute(&converted) == 0); - Require(sceRtcSetMinute(&converted, 59) == 0 && sceRtcGetMinute(&converted) == 59); - - Require(sceRtcGetSecond(nullptr) == invalidPointer); - Require(sceRtcSetSecond(nullptr, 45) == invalidPointer); - Require(sceRtcSetSecond(&converted, -1) == invalidSecond); - Require(sceRtcSetSecond(&converted, 60) == invalidSecond); - Require(sceRtcSetSecond(&converted, 0) == 0 && sceRtcGetSecond(&converted) == 0); - Require(sceRtcSetSecond(&converted, 59) == 0 && sceRtcGetSecond(&converted) == 59); - - Require(sceRtcGetMicrosecond(nullptr) == invalidPointer); - Require(sceRtcSetMicrosecond(nullptr, 123456) == invalidPointer); - Require(sceRtcSetMicrosecond(&converted, -1) == invalidMicrosecond); - Require(sceRtcSetMicrosecond(&converted, 1000000) == invalidMicrosecond); - Require(sceRtcSetMicrosecond(&converted, 0) == 0 && sceRtcGetMicrosecond(&converted) == 0); - Require(sceRtcSetMicrosecond(&converted, 999999) == 0 && sceRtcGetMicrosecond(&converted) == 999999); converted = RtcDateTime{1, 1, 1, 1, 1, 1, 1}; Require(sceRtcSetDosTime(&converted, 0x585d645cu) == 0 && Equal(converted, RtcDateTime{2024, 2, 29, 12, 34, 56, 0})); Require(sceRtcSetDosTime(&converted, 0x7f9fbf7du) == 0 && Equal(converted, RtcDateTime{2043, 12, 31, 23, 59, 58, 0})); diff --git a/core/libs/tests/GuestRudpEmptyPoll.cpp b/core/libs/tests/GuestRudpEmptyPoll.cpp new file mode 100644 index 0000000000..6abdcc3d1b --- /dev/null +++ b/core/libs/tests/GuestRudpEmptyPoll.cpp @@ -0,0 +1,100 @@ +#include "prx/libSceRudp/libSceRudp.hpp" +#include +#include +#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, "empty poll check failed at line %u\n", location.line()); + std::abort(); +} + +static void APS5_VABI Handler(int, int, int, void*) {} + +static void RequireUntouched(const RudpPollEvent& event) { + Require(event.contextId == 0x12345678 && event.events == 0x1234 && event.returnedEvents == 0x5678); +} + +static void CheckTimeout(int poll, std::uint64_t timeout) { + RudpPollEvent event{0x12345678, 0x1234, 0x5678}; + struct Result { + int code; + std::chrono::steady_clock::duration elapsed; + }; + std::promise result; + auto completed = result.get_future(); + std::thread waiter([&] { + const auto start = std::chrono::steady_clock::now(); + const int code = sceRudpPollWait(poll, &event, 1, timeout); + result.set_value({code, std::chrono::steady_clock::now() - start}); + }); + const bool ready = completed.wait_for(std::chrono::seconds(2)) == std::future_status::ready; + if (!ready) sceRudpPollDestroy(poll); + waiter.join(); + Require(ready); + const auto value = completed.get(); + Require(value.code == 0 && value.elapsed >= std::chrono::microseconds(timeout)); + RequireUntouched(event); +} + +static void CheckInfiniteWait() { + const int poll = sceRudpPollCreate(1); + Require(poll > 0); + RudpPollEvent event{0x12345678, 0x1234, 0x5678}; + std::promise started; + auto entered = started.get_future(); + std::promise result; + auto completed = result.get_future(); + std::thread waiter([&] { + started.set_value(); + sceRudpPollWait(poll, &event, 1, std::numeric_limits::max()); + result.set_value(); + }); + entered.wait(); + const bool blocked = completed.wait_for(std::chrono::milliseconds(20)) == std::future_status::timeout; + sceRudpPollDestroy(poll); + const bool woken = completed.wait_for(std::chrono::seconds(2)) == std::future_status::ready; + if (!woken) sceRudpEnd(); + waiter.join(); + Require(blocked && woken); + RequireUntouched(event); +} + +int main() { + constexpr int notInitialized = static_cast(0x80770001u); + constexpr int invalidArgument = static_cast(0x80770004u); + constexpr int invalidPoll = static_cast(0x8077001fu); + alignas(16) static std::array pool{}; + RudpPollEvent event{0x12345678, 0x1234, 0x5678}; + Require(sceRudpPollWait(-1, &event, 1, 0) == notInitialized); + Require(sceRudpPollWait(-1, nullptr, 0, 0) == notInitialized); + RequireUntouched(event); + Require(sceRudpInit_nid_postfix(pool.data(), pool.size()) == 0); + const int poll = sceRudpPollCreate(1); + Require(poll > 0); + Require(sceRudpPollWait(-1, &event, 1, 0) == invalidArgument); + Require(sceRudpPollWait(12345, nullptr, 1, 0) == invalidArgument); + Require(sceRudpPollWait(12345, &event, 0, 0) == invalidArgument); + Require(sceRudpPollWait(12345, &event, 1, 0) == invalidPoll); + RequireUntouched(event); + CheckTimeout(poll, 0); + CheckTimeout(poll, 20000); + CheckTimeout(poll, 50000); + int context = -1; + Require(sceRudpCreateContext(Handler, nullptr, &context) == 0); + Require(sceRudpPollControl(poll, 1, context, 1) == 0); + Require(sceRudpPollControl(poll, 3, context, 0) == 0); + CheckTimeout(poll, 20000); + Require(sceRudpPollControl(poll, 1, context, 1) == 0); + Require(sceRudpTerminate(context) == 0); + CheckTimeout(poll, 20000); + Require(sceRudpPollDestroy(poll) == 0); + CheckInfiniteWait(); + Require(sceRudpEnd() == 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 63fcda1806..c8f243059d 100644 --- a/core/libs/tests/GuestSaveDataDelete.cpp +++ b/core/libs/tests/GuestSaveDataDelete.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -30,15 +29,6 @@ void Check(bool condition, const std::string& what) { ++failures; } -bool Throws(const SaveDataDelete* del) { - try { - sceSaveDataDelete(del); - } catch (const std::runtime_error&) { - return true; - } - return false; -} - int Delete(const char* data, std::size_t size) { SceSaveDataDirName name{}; std::memcpy(name.data, data, size); @@ -91,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"; @@ -132,9 +125,6 @@ int main() { Check(sceSaveDataUmount2(0, &mountResult.mount_point) == 0, "the mounted save unmounts"); } - Check(Throws(nullptr), "null delete struct throws"); - SaveDataDelete nullDir{}; - Check(Throws(&nullDir), "null dir_name pointer throws"); Check(Delete("kept", 5) == 0, "a valid name is deleted"); Check(!std::filesystem::exists(kept.parent_path()), "the valid save directory is gone"); Check(std::filesystem::exists(victim), "the directory beside the save root is untouched"); diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index 182d79a5bd..596fc94026 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -38,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"); @@ -236,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 67ef1daf4a..22a01d0fdb 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -279,6 +279,37 @@ static void CheckRandom() { Require(std::any_of(values.begin() + 1, values.end(), [&](std::uint32_t value) { return value != values[0]; })); } +static void CheckInitParam() { + alignas(8) std::array parameter{}; + const auto* initialize = reinterpret_cast(parameter.data()); + std::uint32_t randomOutput = 0; + Require(sceNetTerm() == 0); + Require(Failed(sceNetGetRandom(&randomOutput), 200)); + Require(Failed(sceNetInitParam(nullptr), 22)); + Require(Failed(sceNetGetRandom(&randomOutput), 200)); + Require(sceNetInitParam(initialize) == 0); + Require(sceNetGetRandom(&randomOutput) == 0); + Require(Failed(sceNetInitParam(initialize), 16)); + Require(Failed(sceNetInitParam(nullptr), 16)); + Require(sceNetTerm() == 0); + Require(Failed(sceNetInitParam(initialize), 16)); + Require(Failed(sceNetGetRandom(&randomOutput), 200)); + Require(sceNetInit_nid_postfix() == 0); +} + +static void CheckIfFlags() { + std::uint32_t flags = 0x5a5a5a5a; + Require(sceNetConfigGetIfFlags(0, &flags) == 0); + Require(flags == 0x8049); + for (const std::uint32_t interfaceId : {10u, 0xffffffffu}) { + flags = 0x5a5a5a5a; + Require(sceNetConfigGetIfFlags(interfaceId, &flags) == -1); + Require(flags == 0x5a5a5a5a); + } + Require(sceNetConfigGetIfFlags(0, nullptr) == static_cast(0x8041130Eu)); + Require(sceNetConfigGetIfFlags(10, nullptr) == static_cast(0x8041130Eu)); +} + static void CheckNetMemory() { *sceNetErrnoLoc() = 33; for (const std::int32_t size : {0, -1, INT32_MIN}) { @@ -505,6 +536,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; @@ -550,6 +615,42 @@ static void CheckDontWait() { Require(sceNetSocketClose(receiver) == 0 && sceNetSocketClose(sender) == 0); } +static void CheckNoSignal() { + constexpr int NoSignal = 0x20000; + constexpr int DontWait = 0x80; + const int listener = sceNetSocket("nosignal-listener", 2, 1, 6); + Require(listener >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0); + Require(sceNetListen(listener, 1) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0); + const int client = sceNetSocket("nosignal-client", 2, 1, 6); + Require(client >= 0); + Require(sceNetConnect(client, address.data(), address.size()) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0); + char reply[] = "nosignal reply"; + Require(sceNetSend(accepted, reply, 4, NoSignal) == 4); + Require(sceNetSendto(accepted, reply + 4, 4, NoSignal | DontWait, nullptr, 0) == 4); + NetIovec out[] = {{reply + 8, sizeof(reply) - 8}}; + NetMsghdr message{nullptr, 0, out, 1, nullptr, 0, 0}; + Require(sceNetSendmsg(accepted, &message, NoSignal) == static_cast(sizeof(reply) - 8)); + char received[sizeof(reply)]{}; + std::size_t total = 0; + while (total < sizeof(reply)) { + const auto count = sceNetRecv(client, received + total, sizeof(received) - total, 0); + Require(count > 0); + total += static_cast(count); + } + Require(std::memcmp(received, reply, sizeof(reply)) == 0); + Require(Failed(sceNetSend(accepted, reply, 1, NoSignal | 0x2), 45)); + Require(sceNetShutdown(accepted, 1) == 0); + Require(Failed(sceNetSend(accepted, reply, 1, NoSignal), 32)); + Require(Failed(sceNetSendto(accepted, reply, 1, NoSignal, nullptr, 0), 32)); + Require(sceNetSocketClose(client) == 0 && sceNetSocketClose(accepted) == 0 && sceNetSocketClose(listener) == 0); +} + static void CheckReceiveTimeout(int receiver, int sender, const std::array& address) { const KernelTimeval timeout{0, 50000}; Require(sceNetSetsockopt(receiver, 0xffff, 0x1006, &timeout, sizeof(timeout)) == 0); @@ -1856,6 +1957,7 @@ int main() { Require(Failed(sceNetShowNetstat(), 200)); Require(Failed(sceNetShowIfconfig(), 200)); Require(sceNetInit_nid_postfix() == 0); + CheckInitParam(); CheckEpollWritable(); CheckSocketHostErrors(); CheckPoolStats(); @@ -1864,7 +1966,10 @@ int main() { CheckNetMemory(); CheckNetstat(); CheckEmptyEpoll(); + CheckIfFlags(); + CheckRejectedArguments(); CheckDontWait(); + CheckNoSignal(); CheckAddressText(2, "127.0.0.1"); CheckAddressText(2, "255.255.255.255"); CheckAddressText(28, "::1"); 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/GuestSchedPriority.cpp b/core/libs/tests/GuestSchedPriority.cpp index 34e03f1f99..27f7e23a90 100644 --- a/core/libs/tests/GuestSchedPriority.cpp +++ b/core/libs/tests/GuestSchedPriority.cpp @@ -73,18 +73,16 @@ int main() { RequireSet(PrioProcess, 0, 0); RequireSet(PrioProcess, self, 5); RequireSet(PrioProcess, self, -20); - RequireSet(PrioGroup, 0, 20); - RequireSet(PrioUser, 0, -1); RequireSetError(PrioProcess, self + 1, 0, 3); RequireSetError(PrioProcess, -1, 0, 3); RequireSetError(PrioGroup, self + 1, 0, 3); RequireSetError(PrioUser, 1000, 0, 3); RequireSetError(3, 0, 0, 22); RequireSetError(-1, self, 0, 22); - Require(setpriority_nid_postfix(PrioProcess, 0, 10) == 0 && getpriority_nid_postfix(PrioProcess, 0) == 10); - Require(setpriority_nid_postfix(PrioProcess, 0, 99) == 0 && getpriority_nid_postfix(PrioProcess, 0) == 20); - Require(setpriority_nid_postfix(PrioProcess, 0, -99) == 0 && getpriority_nid_postfix(PrioProcess, 0) == -20); - Require(setpriority_nid_postfix(PrioProcess, 0, 0) == 0 && getpriority_nid_postfix(PrioProcess, 0) == 0); + for (const int nice : {-25, -5, 5, 10, 25, 99, -99}) { + Require(setpriority_nid_postfix(PrioProcess, 0, nice) == 0 && getpriority_nid_postfix(PrioProcess, 0) == 0); + Require(getpriority_nid_postfix(PrioProcess, self) == 0 && getpriority_nid_postfix(PrioGroup, 0) == 0 && getpriority_nid_postfix(PrioUser, 0) == 0); + } GuestRtPrio rtprio{7, 7}; Require(rtprio_thread_nid_postfix(0, 0, &rtprio) == 0 && rtprio.type == 10 && rtprio.prio == 700); bool rtprioThrew = false; diff --git a/core/libs/tests/GuestSelect.cpp b/core/libs/tests/GuestSelect.cpp index 23cb18d6cf..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}; diff --git a/core/libs/tests/GuestSemaphore.cpp b/core/libs/tests/GuestSemaphore.cpp index bc843e1a5a..89fc2cc852 100644 --- a/core/libs/tests/GuestSemaphore.cpp +++ b/core/libs/tests/GuestSemaphore.cpp @@ -29,6 +29,13 @@ static void RequireInvalidArguments() { Require(sceKernelCreateSema(&rejected, "rejected", 0, 0, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelCreateSema(&rejected, "rejected", 0, -1, 3, nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelCreateSema(&rejected, nullptr, 0, 0, 1, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCreateSema(&rejected, "rejected", 0, 0, -1, nullptr) == SCE_KERNEL_ERROR_EINVAL); + for (std::uint32_t attr = 3; attr <= 0x320; ++attr) { + if (attr >= 0x100 && attr <= 0x102) continue; + Require(sceKernelCreateSema(&rejected, "rejected", attr, 0, 1, nullptr) == SCE_KERNEL_ERROR_EINVAL); + } + for (const std::uint32_t attr : {0x1000u, 0x10000u, 0x80000000u, 0xffffffffu}) + Require(sceKernelCreateSema(&rejected, "rejected", attr, 0, 1, nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(rejected == nullptr); KernelSema sem = nullptr; @@ -39,6 +46,7 @@ static void RequireInvalidArguments() { Require(sceKernelPollSema(sem, 100) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelPollSema(sem, 3) == SCE_KERNEL_ERROR_EBUSY); Require(sceKernelSignalSema(sem, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelSignalSema(sem, -1) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelPollSema(sem, 2) == SCE_OK); Require(sceKernelSignalSema(sem, 3) == SCE_OK); Require(sceKernelPollSema(sem, 3) == SCE_OK); diff --git a/core/libs/tests/GuestSharePlay.cpp b/core/libs/tests/GuestSharePlay.cpp index c1545a1dad..49596cd69a 100644 --- a/core/libs/tests/GuestSharePlay.cpp +++ b/core/libs/tests/GuestSharePlay.cpp @@ -27,6 +27,8 @@ int main() { 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); 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 e2b7017eec..7c6f77cade 100644 --- a/core/libs/tests/GuestSocketAddressLength.cpp +++ b/core/libs/tests/GuestSocketAddressLength.cpp @@ -86,6 +86,17 @@ void LongAddressesAndOtherFamiliesFail() { 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; + const int stream = socket_nid_postfix(2, Stream, 0); + Require(stream >= 0, "socket failed"); + for (const int socket : {datagram, stream}) { + for (const std::uint32_t length : {10u, 28u}) { + Require(Fails(bind_nid_postfix(socket, address.data(), length), ErrorInvalid), "bind on an AF_INET socket checks the length before the family"); + Require(Fails(connect_nid_postfix(socket, address.data(), length), ErrorInvalid), "connect on an AF_INET socket checks the length before the family"); + } + Require(Fails(bind_nid_postfix(socket, address.data(), 16), ErrorAddressFamilyNotSupported), "bind of AF_INET6 with the AF_INET length fails with EAFNOSUPPORT (47)"); + Require(Fails(connect_nid_postfix(socket, address.data(), 16), ErrorAddressFamilyNotSupported), "connect of AF_INET6 with the AF_INET length fails with EAFNOSUPPORT (47)"); + } + Require(close_nid_postfix(stream) == 0, "close failed"); 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; diff --git a/core/libs/tests/GuestSocketDescriptorFlags.cpp b/core/libs/tests/GuestSocketDescriptorFlags.cpp index 6d14f3997a..7f1ae155f0 100644 --- a/core/libs/tests/GuestSocketDescriptorFlags.cpp +++ b/core/libs/tests/GuestSocketDescriptorFlags.cpp @@ -18,6 +18,7 @@ constexpr int Stream = 1; constexpr int SocketCloseOnExec = 0x10000000; constexpr int GuestEbadf = 9; constexpr int GuestEinval = 22; +constexpr int GuestEopnotsupp = 45; void Require(bool condition, const char* what) { if (condition) return; @@ -55,12 +56,11 @@ int main() { Require(minimumDup >= plain + 4 && fcntl_nid_postfix(minimumDup, GetDescriptorFlags) == 0, "F_DUPFD must honor the minimum and clear FD_CLOEXEC"); Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_DUPFD must leave the flag of the original"); Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); - const int cloexecDup = fcntl_nid_postfix(plain, DuplicateDescriptorCloseOnExec, minimumDup + 1); - Require(cloexecDup >= minimumDup + 1 && fcntl_nid_postfix(cloexecDup, GetDescriptorFlags) == CloseOnExec, "F_DUPFD_CLOEXEC must honor the minimum and set FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, DuplicateDescriptorCloseOnExec, minimumDup + 1) == -1 && *__error_nid_postfix() == GuestEopnotsupp, "F_DUPFD_CLOEXEC must fail with EOPNOTSUPP"); Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "F_DUPFD_CLOEXEC must leave the flag of the original"); Require(fcntl_nid_postfix(plain, DuplicateDescriptor, -1) == -1 && *__error_nid_postfix() == GuestEinval, "F_DUPFD with a negative minimum must fail with EINVAL"); - Require(fcntl_nid_postfix(plain, DuplicateDescriptorCloseOnExec, -1) == -1 && *__error_nid_postfix() == GuestEinval, "F_DUPFD_CLOEXEC with a negative minimum must fail with EINVAL"); - Require(close_nid_postfix(cloexecDup) == 0 && close_nid_postfix(minimumDup) == 0, "close failed"); + Require(fcntl_nid_postfix(plain, DuplicateDescriptorCloseOnExec, -1) == -1 && *__error_nid_postfix() == GuestEopnotsupp, "F_DUPFD_CLOEXEC with a negative minimum must fail with EOPNOTSUPP"); + Require(close_nid_postfix(minimumDup) == 0, "close failed"); const int dup2Target = socket_nid_postfix(2, Stream, 0); Require(dup2Target >= 0, "socket failed"); @@ -71,13 +71,14 @@ int main() { Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, plain) == plain && fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_DUP2FD onto the same descriptor must preserve FD_CLOEXEC"); Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, plain) == plain && fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "F_DUP2FD onto the same descriptor must preserve a cleared flag"); - Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, dup2Target) == dup2Target, "F_DUP2FD_CLOEXEC onto another socket must return the target"); - Require(fcntl_nid_postfix(dup2Target, GetDescriptorFlags) == CloseOnExec, "F_DUP2FD_CLOEXEC must set FD_CLOEXEC on the target"); - Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "F_DUP2FD_CLOEXEC must leave the flag of the original"); - Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, plain) == plain && fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_DUP2FD_CLOEXEC onto the same descriptor must set FD_CLOEXEC"); - Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + for (const int target : {dup2Target, plain, -1}) { + const int copy = fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, target); + Require(copy >= 0 && copy != target && copy != plain, "F_DUP2FD_CLOEXEC must ignore the target and return a new descriptor"); + Require(fcntl_nid_postfix(copy, GetDescriptorFlags) == 0, "F_DUP2FD_CLOEXEC must not set FD_CLOEXEC"); + Require(close_nid_postfix(copy) == 0, "close failed"); + } + Require(fcntl_nid_postfix(dup2Target, GetDescriptorFlags) == 0 && fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "F_DUP2FD_CLOEXEC must leave the target and the original alone"); Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, -1) == -1 && *__error_nid_postfix() == GuestEbadf, "F_DUP2FD with a negative target must fail with EBADF"); - Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, -1) == -1 && *__error_nid_postfix() == GuestEbadf, "F_DUP2FD_CLOEXEC with a negative target must fail with EBADF"); Require(close_nid_postfix(dup2Target) == 0, "close failed"); int pipeDescriptors[2] = {-1, -1}; @@ -87,8 +88,9 @@ int main() { Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, pipeDescriptors[0]) == pipeDescriptors[0], "F_DUP2FD onto a pipe must return the target"); Require(fcntl_nid_postfix(pipeDescriptors[0], GetDescriptorFlags) == 0, "F_DUP2FD onto a pipe must clear FD_CLOEXEC"); Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); - Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, pipeDescriptors[1]) == pipeDescriptors[1], "F_DUP2FD_CLOEXEC onto a pipe must return the target"); - Require(fcntl_nid_postfix(pipeDescriptors[1], GetDescriptorFlags) == CloseOnExec, "F_DUP2FD_CLOEXEC onto a pipe must set FD_CLOEXEC"); + const int pipeCopy = fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, pipeDescriptors[1]); + Require(pipeCopy >= 0 && pipeCopy != pipeDescriptors[1] && fcntl_nid_postfix(pipeCopy, GetDescriptorFlags) == 0, "F_DUP2FD_CLOEXEC must leave a pipe target alone"); + Require(close_nid_postfix(pipeCopy) == 0 && fcntl_nid_postfix(pipeDescriptors[1], GetDescriptorFlags) == 0, "close failed"); Require(close_nid_postfix(pipeDescriptors[0]) == 0 && close_nid_postfix(pipeDescriptors[1]) == 0, "close failed"); const int marked = socket_nid_postfix(2, Stream | SocketCloseOnExec, 0); diff --git a/core/libs/tests/GuestSocketDescriptors.cpp b/core/libs/tests/GuestSocketDescriptors.cpp index 3a66b41464..905a290a79 100644 --- a/core/libs/tests/GuestSocketDescriptors.cpp +++ b/core/libs/tests/GuestSocketDescriptors.cpp @@ -177,8 +177,7 @@ static void SocketNumbersStayInDescriptorSet() { Require(dup_nid_postfix(socket) == -1 && *__error_nid_postfix() == 24); Require(fcntl_nid_postfix(socket, DuplicateCommand, 0) == -1 && *__error_nid_postfix() == 24); Require(fcntl_nid_postfix(socket, DuplicateCommand, DescriptorSetSize) == -1 && *__error_nid_postfix() == 24); - Require(fcntl_nid_postfix(socket, DuplicateCloseOnExec, 0) == -1 && *__error_nid_postfix() == 24); - Require(fcntl_nid_postfix(socket, DuplicateCloseOnExec, DescriptorSetSize) == -1 && *__error_nid_postfix() == 24); + Require(fcntl_nid_postfix(socket, DuplicateCloseOnExec, 0) == -1 && *__error_nid_postfix() == 45); Require(socket_nid_postfix(Inet, Datagram, 0) == -1 && *__error_nid_postfix() == 24); Require(dup2_nid_postfix(socket, DescriptorSetSize) == -1 && *__error_nid_postfix() == 24); for (const int file : files) Require(close_nid_postfix(file) == 0); diff --git a/core/libs/tests/GuestSocketOptions.cpp b/core/libs/tests/GuestSocketOptions.cpp index 7c73a6f364..e33092f87d 100644 --- a/core/libs/tests/GuestSocketOptions.cpp +++ b/core/libs/tests/GuestSocketOptions.cpp @@ -151,6 +151,13 @@ void BooleanOptionsReportTheirBit() { std::uint32_t size = sizeof(value); Require(getsockopt_nid_postfix(stream, SolSocket, option, &value, &size) == -1 && *__error_nid_postfix() == ErrorNoProtocolOption, "SO_NO_OFFLOAD and SO_NO_DDP are accepted but cannot be read"); } + for (const int descriptor : {stream, datagram}) + for (const int setting : {5, 0}) { + Require(SetFails(descriptor, SolSocket, 0x1000, &setting, sizeof(setting), ErrorInvalid), "SO_ACCEPTFILTER on a socket that is not listening must fail with EINVAL (22)"); + std::uint64_t filter = 0xbbbbbbbbbbbbbbbbull; + std::uint32_t size = sizeof(filter); + Require(getsockopt_nid_postfix(descriptor, SolSocket, 0x1000, &filter, &size) == -1 && *__error_nid_postfix() == ErrorInvalid && filter == 0xbbbbbbbbbbbbbbbbull, "getsockopt of SO_ACCEPTFILTER on a socket that is not listening must fail with EINVAL (22)"); + } const unsigned char narrowFlag = 1; Require(SetFails(stream, SolSocket, SoDebug, &narrowFlag, 1, ErrorInvalid), "a flag shorter than an int must fail with EINVAL (22)"); Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0, "close failed"); diff --git a/core/libs/tests/GuestSocketPair.cpp b/core/libs/tests/GuestSocketPair.cpp index 126db94d91..e2266c799d 100644 --- a/core/libs/tests/GuestSocketPair.cpp +++ b/core/libs/tests/GuestSocketPair.cpp @@ -201,6 +201,19 @@ int main(int argc, char** argv) { FileStat status{}; Require(fstat_nid_disambig1_nid_postfix(pair[0], &status) == 0); Require(status.st_mode == 0140666 && status.st_size == 0 && status.st_blksize != 0 && status.st_nlink == 0); + Require(status.st_dev == 0xFFFFFFFFu && status.st_ino != 0); + const auto firstInode = status.st_ino; + Require(sceKernelFstat(pair[0], &status) == 0 && status.st_dev == 0xFFFFFFFFu && status.st_ino == firstInode); + Require(fstat_nid_disambig1_nid_postfix(pair[1], &status) == 0 && status.st_dev == 0xFFFFFFFFu); + Require(status.st_ino != 0 && status.st_ino != firstInode); + const auto secondInode = status.st_ino; + const int duplicate = dup_nid_postfix(pair[1]); + Require(duplicate >= 0 && fstat_nid_disambig1_nid_postfix(duplicate, &status) == 0 && status.st_ino == secondInode); + Require(close_nid_postfix(duplicate) == 0); + Require(fstat_nid_disambig1_nid_postfix(pair[1], &status) == 0 && status.st_ino == secondInode); + const int inet = socket_nid_postfix(2, Stream, 0); + Require(inet >= 0 && fstat_nid_disambig1_nid_postfix(inet, &status) == 0 && status.st_dev == 0 && status.st_ino == 0); + Require(close_nid_postfix(inet) == 0); Require(send_nid_postfix(pair[0], "hello", 5, 0) == 5 && Readable(pair[1])); Require(sceKernelFstat(pair[1], &status) == 0 && status.st_mode == 0140666 && status.st_size == 5); Require(shutdown_nid_postfix(pair[0], 1) == 0); diff --git a/core/libs/tests/GuestSocketStreamOptions.cpp b/core/libs/tests/GuestSocketStreamOptions.cpp index 08434c8ae8..647c93cdda 100644 --- a/core/libs/tests/GuestSocketStreamOptions.cpp +++ b/core/libs/tests/GuestSocketStreamOptions.cpp @@ -106,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); diff --git a/core/libs/tests/GuestSoftwareVersion.cpp b/core/libs/tests/GuestSoftwareVersion.cpp index 1343316e32..0dd66f019f 100644 --- a/core/libs/tests/GuestSoftwareVersion.cpp +++ b/core/libs/tests/GuestSoftwareVersion.cpp @@ -15,23 +15,22 @@ static void Require(bool value) { if (!value) std::abort(); } static void CheckProsperoSystemSwVersion() { *__error_nid_postfix() = 13; - Require(sceKernelGetProsperoSystemSwVersion_nid_postfix(nullptr) == SCE_KERNEL_ERROR_EFAULT); - const std::uint64_t sizes[] = {0, 0x18, 0x27, 0x30}; + Require(sceKernelGetProsperoSystemSwVersion_nid_postfix(nullptr) == 0); + const std::uint64_t sizes[] = {0, 0x18, 0x27, 0x28, 0x29, 0x30, 0x100, ~0ull}; for (const std::uint64_t size : sizes) { - SceKernelProsperoSystemSwVersion version{}; - std::memset(&version, 0xaa, sizeof(version)); - version.size = size; - Require(sceKernelGetProsperoSystemSwVersion_nid_postfix(&version) == SCE_KERNEL_ERROR_EINVAL); - Require(version.size == size && version.version == 0xaaaaaaaa && static_cast(version.versionString[0]) == 0xaa); + struct { + SceKernelProsperoSystemSwVersion version; + unsigned char suffix[0x40]; + } storage; + std::memset(&storage, 0xaa, sizeof(storage)); + storage.version.size = size; + Require(sceKernelGetProsperoSystemSwVersion_nid_postfix(&storage.version) == 0); + Require(storage.version.size == size); + Require(std::strcmp(storage.version.versionString, "01.000.000") == 0); + for (std::size_t i = sizeof("01.000.000"); i < sizeof(storage.version.versionString); ++i) Require(storage.version.versionString[i] == 0); + Require(storage.version.version == 0x01000000); + for (const unsigned char byte : storage.suffix) Require(byte == 0xaa); } - SceKernelProsperoSystemSwVersion version{}; - std::memset(&version, 0xaa, sizeof(version)); - version.size = sizeof(version); - Require(sceKernelGetProsperoSystemSwVersion_nid_postfix(&version) == 0); - Require(version.size == sizeof(version)); - Require(std::strcmp(version.versionString, "01.000.000") == 0); - for (std::size_t i = sizeof("01.000.000"); i < sizeof(version.versionString); ++i) Require(version.versionString[i] == 0); - Require(version.version == 0x01000000); Require(*__error_nid_postfix() == 13); } diff --git a/core/libs/tests/GuestSpSysCallWrapper.cpp b/core/libs/tests/GuestSpSysCallWrapper.cpp index 1d22c59592..12f2192bc4 100644 --- a/core/libs/tests/GuestSpSysCallWrapper.cpp +++ b/core/libs/tests/GuestSpSysCallWrapper.cpp @@ -55,6 +55,14 @@ void Threads() { 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); diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index f2d65cf2a4..9925382594 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'); @@ -558,6 +575,10 @@ static int CheckClosedStandardOutput() { Require(printf_nid_postfix("%d", 1) == -1 && *__error_nid_postfix() == 9); Require(puts_nid_postfix("text") == -1 && *__error_nid_postfix() == 9); Require(PrintStandard("%d", 1) == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(printf_s_nid_postfix("%d", 1) == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(printf_s_nid_postfix("%p", nullptr) == -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); @@ -565,6 +586,10 @@ static int CheckClosedStandardOutput() { 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); + *__error_nid_postfix() = 2; + perror_nid_postfix("closed stderr"); + perror_nid_postfix(nullptr); + Require(*__error_nid_postfix() == 2); Require(std::fputs("host error\n", stderr) >= 0 && std::fflush(stderr) == 0 && !std::ferror(stderr)); return 0; } @@ -606,7 +631,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/GuestStreamPurge.cpp b/core/libs/tests/GuestStreamPurge.cpp new file mode 100644 index 0000000000..0303fe2a34 --- /dev/null +++ b/core/libs/tests/GuestStreamPurge.cpp @@ -0,0 +1,128 @@ +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "stream purge: %s\n", message); + std::abort(); + } +} + +std::int64_t Position(std::FILE* stream) { +#ifdef _WIN32 + return ::_telli64(::_fileno(stream)); +#else + return ::lseek(::fileno(stream), 0, SEEK_CUR); +#endif +} + +struct BufferedStream { + std::array buffer{}; + std::FILE* native = std::tmpfile(); + FileStream stream{native}; + + BufferedStream() { Require(std::setvbuf(native, buffer.data(), _IOFBF, buffer.size()) == 0, "set full buffering"); } + ~BufferedStream() { if (stream.IsOpen()) stream.Close(); } +}; + +void CheckOutput() { + BufferedStream file; + Require(fwrite_nid_postfix("discard", 1, 7, &file.stream) == 7, "buffer output"); + Require(Position(file.native) == 0, "output is pending in the stream buffer"); + errno = 123; + Require(fpurge_nid_postfix(&file.stream) == 0 && errno == 123, "purge preserves errno on success"); + Require(Position(file.native) == 0, "purge does not write or seek"); + Require(fpurge_nid_postfix(&file.stream) == 0, "purge an empty buffer twice"); + Require(fwrite_nid_postfix("kept", 1, 4, &file.stream) == 4, "write after purging"); + Require(std::fflush(file.native) == 0 && Position(file.native) == 4, "flush only subsequent output"); + Require(std::fseek(file.native, 0, SEEK_SET) == 0, "rewind subsequent output"); + char bytes[8]{}; + Require(std::fread(bytes, 1, sizeof(bytes), file.native) == 4 && std::memcmp(bytes, "kept", 4) == 0, "discarded output never reaches the file"); +} + +void CheckInput() { + BufferedStream file; + Require(std::fwrite("abcdef", 1, 6, file.native) == 6 && std::fflush(file.native) == 0, "prepare input"); + Require(std::fseek(file.native, 0, SEEK_SET) == 0, "rewind input"); + Require(fgetc_nid_postfix(&file.stream) == 'a', "read one buffered byte"); + const auto position = Position(file.native); + Require(position > 1, "input has read-ahead data"); + Require(ungetc_nid_postfix('X', &file.stream) == 'X', "push back a different byte"); + Require(fpurge_nid_postfix(&file.stream) == 0, "purge input and pushback"); + Require(Position(file.native) == position, "purging input does not reposition the descriptor"); + Require(fgetc_nid_postfix(&file.stream) == EOF, "read-ahead and pushback have been discarded"); + Require((file.stream.GuestState().flags & 0x20) != 0, "publish EOF"); + Require(fpurge_nid_postfix(&file.stream) == 0 && (file.stream.GuestState().flags & 0x20) != 0, "purge preserves EOF"); + file.stream.SetError(); + Require(fpurge_nid_postfix(&file.stream) == 0 && (file.stream.GuestState().flags & 0x60) == 0x60, "purge preserves EOF and error"); +} + +void CheckUnbuffered() { + BufferedStream file; + Require(std::setvbuf(file.native, nullptr, _IONBF, 0) == 0, "disable buffering"); + Require(std::fwrite("ab", 1, 2, file.native) == 2, "write unbuffered input"); + Require(std::fseek(file.native, 0, SEEK_SET) == 0, "rewind unbuffered input"); + Require(fgetc_nid_postfix(&file.stream) == 'a', "read unbuffered byte"); + const auto position = Position(file.native); + Require(position >= 1 && position <= 2, "unbuffered descriptor position"); + Require(ungetc_nid_postfix('X', &file.stream) == 'X', "push back on an unbuffered stream"); + Require(fpurge_nid_postfix(&file.stream) == 0 && Position(file.native) == position, "purge unbuffered pushback without seeking"); + Require(fgetc_nid_postfix(&file.stream) == (position == 1 ? 'b' : EOF), "resume at the actual descriptor position"); +} + +void CheckPipe() { + int descriptors[2]; +#ifdef _WIN32 + Require(::_pipe(descriptors, 4096, _O_BINARY) == 0, "create pipe"); + auto* native = ::_fdopen(descriptors[0], "rb"); + Require(::_write(descriptors[1], "abcdef", 6) == 6 && ::_close(descriptors[1]) == 0, "fill pipe and close writer"); +#else + Require(::pipe(descriptors) == 0, "create pipe"); + auto* native = ::fdopen(descriptors[0], "rb"); + Require(::write(descriptors[1], "abcdef", 6) == 6 && ::close(descriptors[1]) == 0, "fill pipe and close writer"); +#endif + Require(native != nullptr, "open pipe stream"); + std::array buffer{}; + Require(std::setvbuf(native, buffer.data(), _IOFBF, buffer.size()) == 0, "buffer pipe input"); + FileStream stream{native}; + Require(fgetc_nid_postfix(&stream) == 'a' && ungetc_nid_postfix('X', &stream) == 'X', "read and push back on a pipe"); + Require(fpurge_nid_postfix(&stream) == 0, "purge does not require a seekable descriptor"); + Require(fgetc_nid_postfix(&stream) == EOF, "discard pipe read-ahead and pushback"); + Require(stream.Close() == 0, "close pipe stream"); +} +void CheckInvalid() { + BufferedStream file; + Require(file.stream.Close() == 0, "close stream"); + errno = 0; + Require(fpurge_nid_postfix(&file.stream) == -1 && errno == 9, "closed stream reports EBADF"); + bool rejected = false; + try { fpurge_nid_postfix(nullptr); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected, "null stream throws as existing stream functions do"); +} + +} + +int main() { + CheckOutput(); + CheckInput(); + CheckUnbuffered(); + CheckPipe(); + CheckInvalid(); + std::puts("stream purge tests passed"); +} diff --git a/core/libs/tests/GuestStrftime.cpp b/core/libs/tests/GuestStrftime.cpp index cf18e7c687..c7a55368ec 100644 --- a/core/libs/tests/GuestStrftime.cpp +++ b/core/libs/tests/GuestStrftime.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/GuestTime.hpp" #include +#include #include #include #include @@ -165,7 +166,7 @@ int main() { 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"}, + {7, 11, 31, 1, 365, "53|53"}, {126, 9, 24, 6, 296, "42|42"}, {126, 0, 1, 5, INT_MAX, "00|00"}, {126, 0, 1, INT_MAX, 10, "02|03"}, }; for (const auto& week : outOfRangeWeeks) { GuestTm time = base; @@ -179,6 +180,24 @@ int main() { Check(name, time, "%U|%W", week.expected); } + constexpr Iso outOfRangeIsoWeeks[] = { + {126, 0, 1, 5, -25, "2027|27|01"}, {126, 0, 1, 5, -13, "2025|25|00"}, {126, 0, 1, 5, -1, "2025|25|52"}, + {126, 1, 5, 4, 400, "2027|27|01"}, {97, 0, 1, 6, 415, "1997|97|59"}, {113, 11, 31, 1, 365, "2013|13|53"}, + {126, 0, 19, -12, 18, "2026|26|04"}, {100, 11, 28, 1, 362, "2001|01|01"}, {INT_MAX, 0, 1, 4, 400, "0000|52|01"}, + {INT_MIN, 0, 1, 5, -13, "5547|47|00"}, + }; + for (const auto& week : outOfRangeIsoWeeks) { + 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[64]; + std::snprintf(name, sizeof(name), "ISO week %d wday %d yday %d", week.year, week.wday, week.yday); + Check(name, time, "%G|%g|%V", week.expected); + } + char text[8]; std::memset(text, '#', sizeof(text)); if (strftime_nid_postfix(text, 5, "%Y", &base) != 4 || std::strcmp(text, "2026") != 0) { diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index e0effe010e..c7ee8ee32e 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -176,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); 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..9c19aa6e91 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); @@ -17,7 +18,6 @@ static_assert(offsetof(SceKernelSystemSwVersion, versionString) == 0x08); static_assert(offsetof(SceKernelSystemSwVersion, version) == 0x24); static_assert(sizeof(SceKernelSystemSwVersion) == 0x28); extern "C" { - extern char** environ_nid_postfix; } static void Require(bool value) { if (!value) std::abort(); } @@ -96,7 +96,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 +114,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 +147,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", @@ -304,6 +306,47 @@ static void CheckPathnameSysctl() { length = sizeof(buffer); Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); } +static void CheckProcessSysctl() { + const int callerPid = getpid_nid_postfix(); + const int mibPid[] = {1, 14, 1, callerPid}; + std::size_t length = 0; + Require(sysctl_nid_postfix(mibPid, 4, nullptr, &length, nullptr, 0) == 0 && length == 1088); + + unsigned char buffer[1100]; + std::memset(buffer, 0xaa, sizeof(buffer)); + length = sizeof(buffer); + Require(sysctl_nid_postfix(mibPid, 4, buffer, &length, nullptr, 0) == 0 && length == 1088); + std::int32_t structSize = 0; + std::int32_t pid = 0; + std::int32_t parentPid = 0; + std::int64_t flag = -1; + std::memcpy(&structSize, buffer, sizeof(structSize)); + std::memcpy(&pid, buffer + 72, sizeof(pid)); + std::memcpy(&parentPid, buffer + 76, sizeof(parentPid)); + std::memcpy(&flag, buffer + 368, sizeof(flag)); + Require(structSize == 1088 && pid == callerPid && parentPid == getppid_nid_postfix()); + Require((flag & 0x800) == 0); + Require(std::strcmp(reinterpret_cast(buffer + 447), "eboot.bin") == 0); + Require(buffer[4] == 0 && buffer[80] == 0 && buffer[1087] == 0 && buffer[1088] == 0xaa); + + const int mibForeign[] = {1, 14, 1, callerPid + 1000}; + Require(sysctl_nid_postfix(mibForeign, 4, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 3); + + const int mibSelfAlias[] = {1, 14, 1, -1}; + Require(sysctl_nid_postfix(mibSelfAlias, 4, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 3); + + const int mibTooShort[] = {1, 14, 1}; + Require(sysctl_nid_postfix(mibTooShort, 3, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + + const int mibTooLong[] = {1, 14, 1, callerPid, 0}; + Require(sysctl_nid_postfix(mibTooLong, 5, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + + length = sizeof(buffer); + Require(sysctl_nid_postfix(mibPid, 4, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); + + length = 100; + Require(sysctl_nid_postfix(mibPid, 4, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12 && length == 100); +} struct SystemString { int mib[2]; const char* name; @@ -311,9 +354,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 +366,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 +387,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 +399,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)); @@ -524,11 +593,19 @@ static void CheckVersionLimits() { } update{0xaaaaaaaau, 0xaaaaaaaau}; Require(sysKernelGetLowerLimitUpdVersion_nid_postfix(&update.version) == 0 && update.version == 0 && update.after == 0xaaaaaaaau); Require(sysKernelGetLowerLimitUpdVersion_nid_postfix(nullptr) == SCE_KERNEL_ERROR_EINVAL && *__error_nid_postfix() == 13); + + update = {0xaaaaaaaau, 0xaaaaaaaau}; + Require(sysKernelGetUpdVersion_nid_postfix(&update.version) == 0 && update.version == 0 && update.after == 0xaaaaaaaau); + Require(sysKernelGetUpdVersion_nid_postfix(nullptr) == SCE_KERNEL_ERROR_EINVAL && *__error_nid_postfix() == 13); + + Require(sysKernelGetManufacturingMode_nid_postfix() == 0 && *__error_nid_postfix() == 13); + Require(sysKernelGetIntdevModeForInternalLibc_nid_postfix() == 0 && *__error_nid_postfix() == 13); + Require(sysKernelGetIntdevModeForRcmgr_nid_postfix() == 0 && *__error_nid_postfix() == 13); } static void CheckSystemSwVersion() { *__error_nid_postfix() = 13; - Require(sceKernelGetSystemSwVersion_nid_postfix(nullptr) == SCE_KERNEL_ERROR_EFAULT && *__error_nid_postfix() == 13); - const std::uint64_t sizes[] = {0, 24, 39, 40, 41, 48, 64}; + Require(sceKernelGetSystemSwVersion_nid_postfix(nullptr) == 0 && *__error_nid_postfix() == 13); + const std::uint64_t sizes[] = {0, 1, 24, 39, 40, 41, 48, 64, 0x1000, ~0ull}; for (const std::uint64_t size : sizes) { struct { std::uint64_t prefix; @@ -887,18 +964,66 @@ 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(); CheckPathnameSysctl(); + CheckProcessSysctl(); CheckSystemStringSysctl(); CheckSystemStringNames(); CheckOsReleaseDateSysctl(); @@ -921,6 +1046,17 @@ int main() { GuestResourceLimit nofileLimit{}; Require(getrlimit_nid_postfix(8, &nofileLimit) == 0); Require(sysconf_nid_postfix(5) == nofileLimit.rlim_cur); + GuestResourceLimit nprocLimit{}; + Require(getrlimit_nid_postfix(7, &nprocLimit) == 0); + Require(nprocLimit.rlim_cur == 1604); + Require(sysconf_nid_postfix(2) == nprocLimit.rlim_cur); + GuestResourceLimit stackLimit{}; + Require(getrlimit_nid_postfix(3, &stackLimit) == 0); + Require(sysconf_nid_postfix(2) != stackLimit.rlim_cur); + Require(sysconf_nid_postfix(4) == 1023); + Require(sysconf_nid_postfix(26) == std::min(nofileLimit.rlim_cur, 32767)); + Require(sysconf_nid_postfix(72) == 255); + Require(sysconf_nid_postfix(73) == 33); Require(sysconf_nid_postfix(47) == 0x4000); Require(sysconf_nid_postfix(56) == 1024); Require(getpagesize_nid_postfix() == sysconf_nid_postfix(47)); diff --git a/core/libs/tests/GuestSystemFlexibleMemory.cpp b/core/libs/tests/GuestSystemFlexibleMemory.cpp new file mode 100644 index 0000000000..2a42142f40 --- /dev/null +++ b/core/libs/tests/GuestSystemFlexibleMemory.cpp @@ -0,0 +1,88 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "System flexible memory check failed at %s:%u\n", location.file_name(), static_cast(location.line())); + std::abort(); + } +} + +using MapSystemFlexible = int32_t (APS5_VABI*)(void**, std::size_t, int, int, const char*); + +static MapSystemFlexible mapSystemFlexible = nullptr; + +static MapSystemFlexible Resolve() { +#ifdef _WIN32 + const HMODULE module = GetModuleHandleA("libkernel.prx"); + Require(module != nullptr); + auto* symbol = reinterpret_cast(GetProcAddress(module, "sceKernelMapNamedSystemFlexibleMemory")); +#else + auto* symbol = dlsym(RTLD_DEFAULT, "sceKernelMapNamedSystemFlexibleMemory"); +#endif + if (!symbol) { + std::fprintf(stderr, "sceKernelMapNamedSystemFlexibleMemory is not exported by libkernel\n"); + std::abort(); + } + return reinterpret_cast(symbol); +} + +static std::size_t Available() { + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0); + return available; +} + +int main() { + mapSystemFlexible = Resolve(); + constexpr std::size_t length = PS5_PAGE_SIZE * 4; + const std::size_t before = Available(); + void* mapped = nullptr; + Require(mapSystemFlexible(&mapped, length, 3, 0, "system mapping") == 0 && mapped != nullptr); + auto* bytes = static_cast(mapped); + bytes[0] = 17; + bytes[length - 1] = 29; + Require(bytes[0] == 17 && bytes[length - 1] == 29); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(mapped, 0, &info, sizeof(info)) == 0); + Require(std::strcmp(info.name, "system mapping") == 0 && info.protection == 3 && info.is_direct == 0); + Require(Available() == before); + + void* game = nullptr; + Require(sceKernelMapNamedFlexibleMemory(&game, length, 3, 0, "game mapping") == 0 && game != nullptr); + Require(Available() == before - length); + void* replacement = static_cast(game) + PS5_PAGE_SIZE; + void* fixed = replacement; + Require(mapSystemFlexible(&fixed, PS5_PAGE_SIZE, 3, 0x10, "system fixed") == 0 && fixed == replacement); + Require(Available() == before - length + PS5_PAGE_SIZE); + Require(sceKernelMunmap(reinterpret_cast(game), length) == 0); + Require(Available() == before); + + void* invalid = nullptr; + Require(mapSystemFlexible(&invalid, 0, 3, 0, "empty") == SCE_KERNEL_ERROR_EINVAL && invalid == nullptr); + Require(mapSystemFlexible(&invalid, PS5_PAGE_SIZE + 1, 3, 0, "unaligned") == SCE_KERNEL_ERROR_EINVAL && invalid == nullptr); + Require(mapSystemFlexible(&invalid, PS5_PAGE_SIZE + 1, 3, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL && invalid == nullptr); + void* longest = nullptr; + Require(mapSystemFlexible(&longest, PS5_PAGE_SIZE, 3, 0, "abcdefghijklmnopqrstuvwxyz01234") == 0 && longest != nullptr); + Require(sceKernelVirtualQuery(longest, 0, &info, sizeof(info)) == 0 && std::strcmp(info.name, "abcdefghijklmnopqrstuvwxyz01234") == 0); + Require(sceKernelMunmap(reinterpret_cast(longest), PS5_PAGE_SIZE) == 0); + Require(sceKernelMunmap(reinterpret_cast(mapped), length) == 0); + Require(Available() == before); + return 0; +} 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/GuestTimeConversion.cpp b/core/libs/tests/GuestTimeConversion.cpp index abc96e55f2..d51d617b19 100644 --- a/core/libs/tests/GuestTimeConversion.cpp +++ b/core/libs/tests/GuestTimeConversion.cpp @@ -496,6 +496,21 @@ int main() { *__error_nid_postfix() = 0; Require(std::strcmp(ctime_nid_postfix(&underflow), "??? ??? ?? ??:??:?? ????\n") == 0 && *__error_nid_postfix() == 22, "ctime of an unrepresentable time did not report it"); + { + std::barrier finish(3); + std::array texts{}; + std::array readers; + for (std::size_t index = 0; index < readers.size(); ++index) { + readers[index] = std::thread([&, index] { + texts[index] = ctime_nid_postfix(&underflow); + finish.arrive_and_wait(); + }); + } + const char* own = ctime_nid_postfix(&underflow); + finish.arrive_and_wait(); + Require(texts[0] != texts[1] && texts[0] != own && texts[1] != own, "ctime of an unrepresentable time shared one buffer between threads"); + for (auto& reader : readers) reader.join(); + } constexpr std::int64_t latest = std::numeric_limits::max(); constexpr std::int64_t earliest = std::numeric_limits::min(); diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index 5712bea204..a2dd3d37cc 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -1,4 +1,5 @@ #include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/Time/include/Time.hpp" #include "prx/libc/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include @@ -168,9 +169,96 @@ static void CheckDontWait() { Require(close_nid_postfix(receiver) == 0 && close_nid_postfix(sender) == 0); } +static void CheckArrivalTimestamps(int family) { + const std::uint32_t addressSize = family == 2 ? 16 : 28; + const int receiver = socket_nid_postfix(family, 2, 0); + if (receiver < 0 && family == 28 && *__error_nid_postfix() == 47) return; + Require(receiver >= 0); + const int sender = socket_nid_postfix(family, 2, 0); + Require(sender >= 0); + std::array destination{}; + destination[0] = static_cast(addressSize); + destination[1] = static_cast(family); + if (family == 2) { destination[4] = 127; destination[7] = 1; } + else destination[23] = 1; + const int bound = bind_nid_postfix(receiver, destination.data(), addressSize); + if (bound < 0 && family == 28 && *__error_nid_postfix() == 49) { + Require(close_nid_postfix(sender) == 0 && close_nid_postfix(receiver) == 0); + return; + } + Require(bound == 0); + std::uint32_t size = destination.size(); + Require(getsockname_nid_postfix(receiver, destination.data(), &size) == 0); + Require(connect_nid_postfix(sender, destination.data(), addressSize) == 0); + for (const int options : {0, 0x400, 0x2000, 0x2400}) { + for (const int option : {0x400, 0x2000}) { + const int enabled = (options & option) != 0 ? 1 : 0; + Require(setsockopt_nid_postfix(receiver, 0xffff, option, &enabled, sizeof(enabled)) == 0); + } + const bool binary = family == 2 && (options & 0x2000) != 0; + const bool micro = (options & 0x400) != 0; + const std::uint32_t expected = (binary ? 32u : 0u) + (micro ? 32u : 0u); + for (const std::uint32_t capacity : {0u, 8u, 16u, 40u, 64u, 80u}) { + const char payload[] = "stamped"; + Require(sendto_nid_postfix(sender, payload, sizeof(payload), 0, nullptr, 0) == sizeof(payload)); + std::array control; + control.fill(0xa5); + char received[sizeof(payload)]{}; + KernelIovec vector{received, sizeof(received)}; + GuestMsghdr message{nullptr, 0, &vector, 1, control.data(), capacity, -1}; + KernelTimespec before{}; + Require(clock_gettime_nid_postfix(0, &before) == 0); + Require(recvmsg_nid_postfix(receiver, &message, 0) == sizeof(payload)); + KernelTimespec after{}; + Require(clock_gettime_nid_postfix(0, &after) == 0); + Require(std::memcmp(received, payload, sizeof(payload)) == 0); + const std::uint32_t copied = std::min(capacity, expected); + Require(message.controlLength == copied); + Require(message.flags == (capacity < expected ? 0x20 : 0)); + Require(std::all_of(control.begin() + copied, control.end(), [](auto byte) { return byte == 0xa5; })); + std::array whole{}; + std::memcpy(whole.data(), control.data(), copied); + std::uint32_t at = 0; + for (const int type : {4, 2}) { + if ((type == 4 && !binary) || (type == 2 && !micro)) continue; + if (at + 32 > copied) break; + std::uint32_t length = 0; + int level = 0; + int kind = 0; + std::int64_t seconds = 0; + std::uint64_t fraction = 0; + std::memcpy(&length, whole.data() + at, sizeof(length)); + std::memcpy(&level, whole.data() + at + 4, sizeof(level)); + std::memcpy(&kind, whole.data() + at + 8, sizeof(kind)); + std::memcpy(&seconds, whole.data() + at + 16, sizeof(seconds)); + std::memcpy(&fraction, whole.data() + at + 24, sizeof(fraction)); + Require(length == 32 && level == 0xffff && kind == type); + Require(seconds >= before.tv_sec && seconds <= after.tv_sec); + if (type == 2) Require(fraction < 1000000); + at += 32; + } + if (copied >= 8) { + std::uint32_t length = 0; + std::memcpy(&length, whole.data(), sizeof(length)); + Require(length == 32); + } + } + const char payload[] = "unstamped"; + Require(sendto_nid_postfix(sender, payload, sizeof(payload), 0, nullptr, 0) == sizeof(payload)); + char received[sizeof(payload)]{}; + KernelIovec vector{received, sizeof(received)}; + GuestMsghdr message{nullptr, 0, &vector, 1, nullptr, 0, -1}; + Require(recvmsg_nid_postfix(receiver, &message, 0) == sizeof(payload)); + Require(message.controlLength == 0 && message.flags == 0); + } + Require(close_nid_postfix(sender) == 0 && close_nid_postfix(receiver) == 0); +} + int main() { CheckAddressLengths(2); CheckAddressLengths(28); + CheckArrivalTimestamps(2); + CheckArrivalTimestamps(28); CheckDontWait(); CheckSendToPortZero(); CheckShutReadUnconnected(); 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/GuestUpdateEnv.cpp b/core/libs/tests/GuestUpdateEnv.cpp index 5de6f53321..b6aa646785 100644 --- a/core/libs/tests/GuestUpdateEnv.cpp +++ b/core/libs/tests/GuestUpdateEnv.cpp @@ -253,6 +253,23 @@ void Pending(unsigned bit,unsigned source) { Require(false,"enabled pending exception did not trap during feupdateenv"); } +void DefaultEnvironment(const Environment* fallback) { + Require(fallback!=nullptr,"__fe_dfl_env is missing"); + constexpr unsigned char measured[32]{0x7f,0x03,0xff,0xff,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0xff,0xff,0xc0,0x9f,0x00,0x00}; + Require(std::memcmp(fallback,measured,sizeof(measured))==0,"__fe_dfl_env bytes"); + const auto original=Read(); + auto disturbed=original; + disturbed.x87[0]=(disturbed.x87[0]&~0xf3fu)|0xc00u|0x3fu; + disturbed.x87[1]&=~0xffu; + disturbed.mxcsr=(disturbed.mxcsr&~0x7fffu)|0x6000u|(0x3f<<7); + Write(disturbed); + Require(updateEnvironment(fallback)==0,"feupdateenv(__fe_dfl_env) result"); + const auto restored=Read(); + Require((restored.x87[0]&0xffffu)==0x037fu,"__fe_dfl_env x87 control"); + Require((restored.x87[1]&0x3fu)==0 && restored.mxcsr==0x9fc0u,"__fe_dfl_env leaves no flags when none were pending"); + Write(original); +} + int main(int argc,char** argv) { Require(argc==2 || argc==4,"expected libc path and optional trap bit/source"); #ifdef _WIN32 @@ -265,8 +282,13 @@ int main(int argc,char** argv) { updateEnvironment=reinterpret_cast(dlsym(library,"feupdateenv_nid_postfix")); #endif Require(updateEnvironment!=nullptr,"export is missing"); +#ifdef _WIN32 + const auto fallback=reinterpret_cast(GetProcAddress(library,"__fe_dfl_env_nid_postfix")); +#else + const auto fallback=static_cast(dlsym(library,"__fe_dfl_env_nid_postfix")); +#endif if(argc==4) Pending(static_cast(std::atoi(argv[2])),static_cast(std::atoi(argv[3]))); - Matrix(); TagsAndPointers(); BoundsAndErrors(); MasksAndThreads(); + Matrix(); TagsAndPointers(); BoundsAndErrors(); MasksAndThreads(); DefaultEnvironment(fallback); #ifdef _WIN32 Require(FreeLibrary(library)!=0,"release library lookup handle"); #else 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/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/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index f6caa14b4b..8537ed2d6c 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -578,6 +578,53 @@ void testEmptyAccessUnit() { check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); } +void testDiscardedFrameFlag() { + static_assert(sizeof(OutputInfo) == 0x38 && offsetof(OutputInfo, pictureCount) == 0xa && offsetof(OutputInfo, codecType) == 0xc); + std::vector buffer(frameBufferBytes()); + const auto decodeAll = [&](std::uint64_t handle, const std::vector>& units, std::size_t expected, bool lastPicturesFromFlush) { + std::size_t pictures = 0; + std::size_t flushed = 0; + const auto call = [&](const InputData* input) { + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output; + std::memset(&output, 0xff, sizeof(output)); + output.thisSize = sizeof(OutputInfo); + const auto result = input ? sceVideodec2Decode_nid_postfix(handle, input, &frame, &output) : sceVideodec2Flush_nid_postfix(handle, &frame, &output); + check(result == 0, "decode or flush failed"); + std::uint8_t discarded = 0; + std::memcpy(&discarded, reinterpret_cast(&output) + 0xb, 1); + check(!output.isValid || discarded == 0, "a returned picture is marked as discarded"); + check(output.isValid || discarded == 0xff, "a result without a picture wrote the discarded flag"); + if (output.isValid) { + ++pictures; + if (!input) ++flushed; + } + return output.isValid; + }; + { + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output; + std::memset(&output, 0xff, sizeof(output)); + output.thisSize = sizeof(OutputInfo); + const InputData empty{sizeof(InputData), units[0].data(), 0, 0, 0, 0}; + check(sceVideodec2Decode_nid_postfix(handle, &empty, &frame, &output) == static_cast(0x811D010Du), "an empty access unit was accepted"); + std::uint8_t discarded = 0; + std::memcpy(&discarded, reinterpret_cast(&output) + 0xb, 1); + check(discarded == 0xff, "a refused access unit wrote the discarded flag"); + } + for (std::size_t unit = 0; unit < units.size(); ++unit) { + const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), 1000 + unit, unit, 0}; + call(&input); + } + while (call(nullptr)) {} + check(pictures == expected, "a picture is missing"); + check(!lastPicturesFromFlush || flushed > 0, "no picture came back from Flush"); + check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); + }; + decodeAll(createDecoder(CodecVp9, 0), ivfFrames(), Vp9Hashes.size(), false); + decodeAll(createDecoder(1, 100), accessUnits(false), PictureHashes.size(), true); +} + void testFlushAfterEveryUnit() { std::vector> units; for (std::size_t index = 0; index + 4 < AvcIpppStream.size(); ++index) { @@ -788,6 +835,7 @@ int main() { testTenBit(); testDecodeAfterFlush(); testEmptyAccessUnit(); + testDiscardedFrameFlag(); testFlushAfterEveryUnit(); testPictureOwnership(); testConcurrentOwnership(); diff --git a/core/libs/tests/GuestVoiceChat.cpp b/core/libs/tests/GuestVoiceChat.cpp index 0b80ea67e8..7665ef4423 100644 --- a/core/libs/tests/GuestVoiceChat.cpp +++ b/core/libs/tests/GuestVoiceChat.cpp @@ -5,12 +5,6 @@ #include #include -extern "C" { - -int APS5_VABI sceVoiceChatRequestAcsTts(int requestId, void* a, void* b); - -} - static void Require(bool value, const char* what) { if (value) return; std::printf("VoiceChat: %s\n", what); @@ -22,8 +16,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/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..5540d7ee60 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 @@ -137,6 +138,12 @@ static void CheckCount() { Require(Format(buffer, 16, u"a%n", nullCount) < 0, "vswprintf null %n argument"); } +static void CheckUnsignedSignFlags() { + char16_t buffer[32]; + Require(swprintf_nid_postfix(buffer, 32, u"[% u|%+u|% lu|% 5u|%+x|% #o]", 7u, 7u, 7ul, 7u, 7u, 7u) == 18 && + buffer == std::u16string(u"[7|7|7| 7|7|07]"), "unsigned conversions ignore the space and plus flags"); +} + static void CheckGroupingFlag() { char16_t buffer[48]; Require(swprintf_nid_postfix(buffer, 48, u"%'d|%'-6u|%'.2f|%'+05d|%'g", 1234567, 42u, 1234.5, 12, 1e6) == 34 && @@ -177,6 +184,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 +315,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"); @@ -316,4 +346,5 @@ int main() { Check(u"%.1s", "\xf0\x9f\x98\x80x", u""); Check(u"%.3s", "\xf0\x9f\x98\x80x", u"\U0001f600x"); CheckGroupingFlag(); + CheckUnsignedSignFlags(); } 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/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/ResolverRaw.cpp b/core/libs/tests/ResolverRaw.cpp new file mode 100644 index 0000000000..0411d8b5b4 --- /dev/null +++ b/core/libs/tests/ResolverRaw.cpp @@ -0,0 +1,81 @@ +#include "prx/libScePosixForWebKit/libScePosixForWebKit.hpp" +#include "prx/libScePosixForWebKit/GuestResolver.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + + +namespace { + +void Require(bool result, const char* message) { + if (result) return; + std::fprintf(stderr, "raw resolver: %s\n", message); + std::abort(); +} + +void Address(const char* node, int family, int protocol, int flags) { + GuestResolver::AddressInfo hints{}; + hints.flags = flags; + hints.family = family; + hints.socketType = 3; + hints.protocol = protocol; + GuestResolver::AddressInfo* result = nullptr; + Require(getaddrinfo_nid_postfix(node, nullptr, &hints, &result) == 0 && result, "raw address resolution failed"); + unsigned count = 0; + for (const auto* entry = result; entry; entry = entry->next) { + ++count; + Require(entry->socketType == 3 && entry->protocol == protocol, "raw type or protocol changed"); + const auto* bytes = static_cast(entry->address); + Require(entry->family == (node[0] == ':' ? 28 : 2), "address family changed"); + Require(bytes[0] == entry->addressLength && bytes[1] == entry->family, "guest sockaddr prefix differs"); + Require(bytes[2] == 0 && bytes[3] == 0, "raw address has a transport port"); + if (entry->family == 2) { + constexpr std::array expected{127, 0, 0, 1}; + Require(entry->addressLength == 16 && std::memcmp(bytes + 4, expected.data(), expected.size()) == 0, "IPv4 bytes differ"); + } else { + constexpr std::array expected{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; + Require(entry->addressLength == 28 && std::memcmp(bytes + 8, expected.data(), expected.size()) == 0, "IPv6 bytes differ"); + } + if (flags & 2) Require(entry->canonicalName && std::strcmp(entry->canonicalName, node) == 0, "numeric canonical name differs"); + } + Require(count == 1, "raw numeric query returned duplicate socket variants"); + Require(hints.socketType == 3 && hints.protocol == protocol && hints.flags == flags && hints.family == family, "query mutated hints"); + freeaddrinfo_nid_postfix(result); +} + +} + +int main() { + for (int protocol : {0, 1, 6, 17, 58, 132, 255, 256, -1, std::numeric_limits::min(), std::numeric_limits::max()}) { + for (int flags : {4, 6, 12}) { + Address("127.0.0.1", 0, protocol, flags); + Address("127.0.0.1", 2, protocol, flags); + Address("::1", 0, protocol, flags); + Address("::1", 28, protocol, flags); + } + } + GuestResolver::AddressInfo hints{}; + hints.flags = 4; + hints.socketType = 3; + for (const char* service : {"", "0", "123", "65535", "http", "not-a-service"}) { + auto* result = reinterpret_cast(1); + Require(getaddrinfo_nid_postfix("127.0.0.1", service, &hints, &result) == 9 && !result, "raw service did not fail with EAI_SERVICE and clear output"); + } + GuestResolver::AddressInfo* result = nullptr; + Require(getaddrinfo_nid_postfix(nullptr, nullptr, &hints, &result) == 8 && !result, "missing host and service was accepted"); + hints.family = 2; + Require(getaddrinfo_nid_postfix("::1", nullptr, &hints, &result) != 0 && !result, "family mismatch was accepted"); + hints.family = 0; + hints.flags = 0x200; + Require(getaddrinfo_nid_postfix("127.0.0.1", nullptr, &hints, &result) == 3 && !result, "invalid raw flags were accepted"); + Require(getaddrinfo_nid_postfix(nullptr, nullptr, &hints, &result) == 8 && !result, "invalid flags overrode missing names"); + hints.flags = 4; + hints.addressLength = 1; + Require(getaddrinfo_nid_postfix("127.0.0.1", nullptr, &hints, &result) == 12 && !result, "invalid raw hint state was accepted"); + Require(getaddrinfo_nid_postfix(nullptr, nullptr, &hints, &result) == 8 && !result, "invalid hint state overrode missing names"); + return 0; +} 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/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 047e7616b7..cec7f2e98d 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -109,6 +109,16 @@ if(BUILD_TESTING) target_link_libraries(relinker_conversion_tests PRIVATE relinker_core) add_test(NAME relinker_conversion COMMAND relinker_conversion_tests) set_tests_properties(relinker_conversion PROPERTIES TIMEOUT 20) + + add_test_executable(native_path_tests + ${CMAKE_CURRENT_SOURCE_DIR}/io/tests/NativePathTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/NativePath.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileReader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileWriter.cpp) + target_include_directories(native_path_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/io/include ${CMAKE_CURRENT_SOURCE_DIR}/domain/include) + add_test(NAME native_path COMMAND native_path_tests) + set_tests_properties(native_path PROPERTIES TIMEOUT 20) + add_test_executable(relinker_game_patch_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/GamePatchTests.cpp) target_link_libraries(relinker_game_patch_tests PRIVATE relinker_core) add_test(NAME relinker_game_patch COMMAND relinker_game_patch_tests) @@ -305,11 +315,11 @@ if(BUILD_TESTING) add_test_executable(red_zone_framer_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/RedZoneFramerTests.cpp) target_link_libraries(red_zone_framer_tests PRIVATE relinker_core) add_test(NAME red_zone_framer COMMAND red_zone_framer_tests) - if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") + if((CMAKE_SYSTEM_NAME STREQUAL "Linux" OR WIN32) AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") add_test_executable(red_zone_framer_execution_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/RedZoneFramerExecutionTests.cpp) target_link_libraries(red_zone_framer_execution_tests PRIVATE relinker_core) add_test(NAME red_zone_framer_execution COMMAND red_zone_framer_execution_tests) - set_tests_properties(red_zone_framer_execution PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) + set_tests_properties(red_zone_framer_execution PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) endif() add_test_executable(cli_args_tests @@ -354,6 +364,18 @@ if(BUILD_TESTING) add_test(NAME file_identity COMMAND file_identity_tests) set_tests_properties(file_identity PROPERTIES TIMEOUT 20) + add_test_executable(file_reader_tests + ${CMAKE_CURRENT_SOURCE_DIR}/io/tests/FileReaderTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileReader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/NativePath.cpp + ) + target_include_directories(file_reader_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ) + add_test(NAME file_reader_prefix COMMAND file_reader_tests) + set_tests_properties(file_reader_prefix PROPERTIES TIMEOUT 20) + add_test_executable(param_json_utf8_tests ${CMAKE_CURRENT_SOURCE_DIR}/../libs/tests/ParamJsonParser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp) @@ -548,7 +570,7 @@ if(BUILD_TESTING) foreach(opcode IN ITEMS add or adc sbb and sub xor cmp) list(APPEND tlsCoverageGroups alu_${opcode}) endforeach() - list(APPEND tlsCoverageGroups gs_traps gs_traps_implicit) + list(APPEND tlsCoverageGroups gs_traps gs_traps_implicit gs_non_memory) foreach(group IN LISTS tlsCoverageGroups) add_test(NAME tls_function_coverage_${group} COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_tls_function_coverage.py $ ${group}) @@ -564,6 +586,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} @@ -600,12 +624,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} @@ -627,6 +656,8 @@ if(BUILD_TESTING) ${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} @@ -644,6 +675,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} @@ -758,6 +791,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} @@ -920,12 +954,6 @@ if(BUILD_TESTING) ) add_test(NAME reloc_table_bounds COMMAND reloc_table_bounds_tests $) endif() - - set(relinkerTestTemp ${CMAKE_CURRENT_BINARY_DIR}/test-temp) - file(MAKE_DIRECTORY ${relinkerTestTemp}) - get_property(relinkerTests DIRECTORY PROPERTY TESTS) - set_property(TEST ${relinkerTests} APPEND PROPERTY ENVIRONMENT_MODIFICATION - "TMPDIR=set:${relinkerTestTemp};TEMP=set:${relinkerTestTemp};TMP=set:${relinkerTestTemp}") endif() if(BUILD_TESTING AND Python3_Interpreter_FOUND) @@ -936,3 +964,11 @@ if(BUILD_TESTING AND Python3_Interpreter_FOUND) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_guest_weak_imports.py $) set_property(TEST macos_guest_weak_imports APPEND PROPERTY LABELS relinker) endif() + +if(BUILD_TESTING) + set(relinkerTestTemp ${CMAKE_CURRENT_BINARY_DIR}/test-temp) + file(MAKE_DIRECTORY ${relinkerTestTemp}) + get_property(relinkerTests DIRECTORY PROPERTY TESTS) + set_property(TEST ${relinkerTests} APPEND PROPERTY ENVIRONMENT_MODIFICATION + "TMPDIR=set:${relinkerTestTemp};TEMP=set:${relinkerTestTemp};TMP=set:${relinkerTestTemp};PYTHONUTF8=set:1") +endif() 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 6ab190a572..9990cfb485 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -50,6 +50,7 @@ static std::uint64_t ParseFixedBase(std::string_view value, const std::uint64_t Args ParseArgs(int argc, char* argv[]) { Args args; bool unusedFilterSpecified = false; + bool runPathSpecified = false; bool sceModulePathSpecified = false; std::vector givenOptions; @@ -131,9 +132,11 @@ Args ParseArgs(int argc, char* argv[]) { } else if (arg == "--registry") { args.writeRegistry = true; } else if (arg == "--rpath") { + if (runPathSpecified) throw std::runtime_error("--rpath cannot be repeated"); if (i + 1 >= argc) throw std::runtime_error("--rpath requires a value"); args.runPath = argv[++i]; + runPathSpecified = true; } else if (arg == "--windows") { args.toWindows = true; } else if (arg == "--macos") { @@ -193,14 +196,14 @@ Args ParseArgs(int argc, char* argv[]) { throw std::runtime_error("--to-rosetta conflicts with --windows"); if (args.windowsDiagnostics && !args.toWindows) throw std::runtime_error("--windows-diagnostics requires --windows"); + if (args.frameRedZone && !args.toWindows) + throw std::runtime_error("--frame-red-zone requires --windows"); if (args.windowsGui && !args.toWindows) throw std::runtime_error("--windows-gui requires --windows"); 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 6163063c69..2e0822b130 100644 --- a/core/relinker/cli/src/NativeModules.cpp +++ b/core/relinker/cli/src/NativeModules.cpp @@ -115,31 +115,9 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil 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; - const auto soname = [&](std::size_t index) -> const std::string& { - const auto [found, inserted] = sonames.try_emplace(index); - if (inserted) found->second = GuestImageReader().Read(result.Modules[index].Path, result.Modules[index].Bytes, true).Soname; - return found->second; - }; - std::vector copies(result.Modules.size()); - for (std::size_t copy = 0; copy < result.Modules.size(); ++copy) { - const auto& path = result.Modules[copy].Path; - const auto name = Io::Utf8Path(path.filename()); - for (std::size_t original = 0; original < result.Modules.size(); ++original) { - const auto& kept = result.Modules[original].Path; - const auto prefix = Io::Utf8Path(kept.filename()); - if (kept.parent_path() != path.parent_path() || prefix.size() >= name.size() || !name.starts_with(prefix)) continue; - if (soname(copy).empty() || soname(copy) != soname(original)) continue; - copies[copy] = true; - std::cout << "Skipping backup copy: " << path.lexically_relative(root).generic_string() << " (same SONAME as " - << kept.lexically_relative(root).generic_string() << ")\n"; - break; - } - } - std::vector modules; - for (std::size_t index = 0; index < result.Modules.size(); ++index) - if (!copies[index]) modules.push_back(std::move(result.Modules[index])); - result.Modules = std::move(modules); + for (const auto& [copy, kept] : RemoveBackupCopies(result.Modules)) + std::cout << "Skipping backup copy: " << copy.lexically_relative(root).generic_string() << " (same SONAME as " + << kept.lexically_relative(root).generic_string() << ")\n"; return result; } 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 3cd3069dc1..da2a62342b 100644 --- a/core/relinker/cli/tests/CliArgsTests.cpp +++ b/core/relinker/cli/tests/CliArgsTests.cpp @@ -199,6 +199,14 @@ void windowsGuiRequiresWindows() { require(ok.windowsGui && ok.toWindows, "--windows-gui with --windows was rejected"); } +void frameRedZoneRequiresWindows() { + const std::string message = "--frame-red-zone requires --windows"; + expectThrows({"--frame-red-zone", "input.elf", "output.elf"}, message); + expectThrows({"--macos", "--frame-red-zone", "input.elf", "output.elf"}, message); + const auto ok = parse({"--windows", "--frame-red-zone", "input.elf", "output.elf"}); + require(ok.frameRedZone && ok.toWindows, "--frame-red-zone with --windows was rejected"); +} + void missingPositionals() { expectUsage({}); expectUsage({"input.elf"}); @@ -361,6 +369,7 @@ int main() { tooManyPositionals(); windowsDiagnosticsRequiresWindows(); windowsGuiRequiresWindows(); + frameRedZoneRequiresWindows(); missingPositionals(); windowsFixedBase(); linuxFixedBase(); diff --git a/core/relinker/codegen/include/codegen/x86/RedZoneFramer.hpp b/core/relinker/codegen/include/codegen/x86/RedZoneFramer.hpp index a23fa23206..fac86f784d 100644 --- a/core/relinker/codegen/include/codegen/x86/RedZoneFramer.hpp +++ b/core/relinker/codegen/include/codegen/x86/RedZoneFramer.hpp @@ -22,6 +22,9 @@ struct RedZoneFrameReport { std::size_t Framed = 0; std::size_t FramedWithoutFramePointer = 0; std::size_t Exits = 0; + std::size_t WidenedPrologues = 0; + std::size_t WidenedExits = 0; + std::size_t WidenedOperands = 0; std::size_t MovedOperands = 0; std::size_t OperandStubs = 0; std::size_t StubbedOperands = 0; 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/Amd64OnlyConverter.cpp b/core/relinker/codegen/src/Amd64OnlyConverter.cpp index de510c4224..8264fb4db1 100644 --- a/core/relinker/codegen/src/Amd64OnlyConverter.cpp +++ b/core/relinker/codegen/src/Amd64OnlyConverter.cpp @@ -103,6 +103,7 @@ private: [[nodiscard]] std::uint64_t _address(const std::size_t index) const { return _ph.MappedAddress + _matches[index].Offset; } [[nodiscard]] std::uint64_t _end(const std::size_t index) const { return _address(index) + _matches[index].Length; } + [[nodiscard]] static std::int64_t _distance(const std::uint64_t to, const std::uint64_t from) { return static_cast(to - from); } [[nodiscard]] std::span _bytes(const std::size_t index) const { return {_seg.data() + _matches[index].Offset, _matches[index].Length}; } [[nodiscard]] DecodedInstructionInfo _info(const std::size_t index) const { return _decoder.DecodeInstruction(_seg.data() + _matches[index].Offset, _matches[index].Length); } @@ -166,7 +167,7 @@ private: if (head == 0 && _end(last) - _address(start) >= kJmpRel32.Size) head = static_cast(_end(last) - _address(start)); const auto island = _address(start) + head; - const auto reach = static_cast(island) - static_cast(_address(index) + kShortJumpSize); + const auto reach = _distance(island, _address(index) + kShortJumpSize); if (head != 0 && (reach < std::numeric_limits::min() || reach > std::numeric_limits::max())) return std::nullopt; if (head != 0 && _end(last) >= island + kJmpRel32.Size) { @@ -200,9 +201,9 @@ private: placement.Sites.push_back(_site(windowStart, head, moved.Finish())); StubBodyBuilder lowered; - lowered.Seek(static_cast(store) - static_cast(island)); + lowered.Seek(_distance(store, island)); _matcher.EmitOutOfLine(lowered, _bytes(index)); - lowered.JumpTo(static_cast(_end(index)) - static_cast(island)); + lowered.JumpTo(_distance(_end(index), island)); placement.Sites.push_back(_site(island, kJmpRel32.Size, lowered.Finish())); placement.ReplacementLength = placement.Sites.back().Body.size(); placement.Claimed.push_back(index); @@ -210,7 +211,7 @@ private: const auto offset = _matches[index].Offset; std::fill_n(_seg.begin() + static_cast(offset), _matches[index].Length, Amd64OnlySubstitutionTable::kTrapFill); _seg[offset] = X64OpcodeConstants::OneByteJmpRel8; - _seg[offset + 1] = static_cast(static_cast(static_cast(island) - static_cast(store + kShortJumpSize))); + _seg[offset + 1] = static_cast(static_cast(_distance(island, store + kShortJumpSize))); return placement; } }; diff --git a/core/relinker/codegen/src/x86/RedZoneFramer.cpp b/core/relinker/codegen/src/x86/RedZoneFramer.cpp index cfe714faef..b77b4341da 100644 --- a/core/relinker/codegen/src/x86/RedZoneFramer.cpp +++ b/core/relinker/codegen/src/x86/RedZoneFramer.cpp @@ -47,6 +47,16 @@ std::optional poppedRegister(const std::uint8_t* code, const Insn& return std::nullopt; } +bool pushesOrPops(const std::uint8_t* code, const Insn& insn) { + if (pushedRegister(code, insn) || poppedRegister(code, insn)) return true; + const auto& info = insn.Info; + const auto lead = code[info.OpcodeOffset]; + if (info.RexPrefix == 0 && (lead == 0xc4 || lead == 0xc5 || lead == 0x62 || lead == 0x8f) && info.Opcode != 0x8f) return false; + if (info.IsTwoByteOpcode) return info.Opcode == 0xa0 || info.Opcode == 0xa1 || info.Opcode == 0xa8 || info.Opcode == 0xa9; + const auto reg = static_cast((info.ModRmByte >> 3) & 7); + return (info.Opcode >= 0x50 && info.Opcode <= 0x5f) || info.Opcode == 0x68 || info.Opcode == 0x6a || info.Opcode == 0x9c || info.Opcode == 0x9d || (info.Opcode == 0x8f && info.HasModRm && reg == 0) || (info.Opcode == 0xff && info.HasModRm && reg == 6); +} + std::optional rspAdjustment(const std::uint8_t* code, const Insn& insn) { std::int32_t value = 0; if (insn.Length == 4 && code[0] == 0x48 && code[1] == 0x83 && (code[2] == 0xec || code[2] == 0xc4)) { @@ -78,6 +88,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 +103,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 +143,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; @@ -208,6 +223,16 @@ struct OperandStub { std::size_t Operands = 0; }; +struct ExitSite { + std::size_t First; + std::size_t Tail; + std::size_t Begin; + std::size_t TailBegin; + std::size_t End; + std::vector Lead; + std::size_t Operands = 0; +}; + void frameSegment( const std::vector& fileBytes, const Domain::ProgramHeader& ph, @@ -286,7 +311,7 @@ void frameSegment( bool leaf = true; bool redZone = false; const char* reason = nullptr; - std::vector> exits; + std::vector exits; std::map>> moves; std::vector stubbed; const auto functionEnd = limit; @@ -310,6 +335,7 @@ void frameSegment( if (use.RspBased && use.RspDisp < 0) redZone = true; if (reason != nullptr) continue; if (code[insn.Info.OpcodeOffset] == 0xc9 || code[insn.Info.OpcodeOffset] == 0xc8) { reason = "leave or enter"; continue; } + if (!rbpPrologue && pushed.empty() && allocation == 0 && pushesOrPops(code, insn)) { reason = "push or pop in a leaf without pushes"; continue; } if (rbpPrologue) { if (use.UsesRsp) { reason = "other use of rsp"; continue; } } else { @@ -335,24 +361,60 @@ void frameSegment( if (q == p || !frees.contains(q - 1)) { reason = "exit does not free the prologue's allocation"; continue; } --q; } - const auto siteStart = body[q].Offset; - const auto siteEnd = insn.Offset + 1; - if (siteEnd - siteStart < kJumpBytes) { reason = "exit shorter than a jump"; continue; } - if (targetInside(siteStart, siteEnd)) { reason = "branch into an exit"; continue; } - exits.emplace_back(siteStart, siteEnd); + ExitSite exit{q, q, body[q].Offset, body[q].Offset, insn.Offset + 1, {}}; + while (exit.End - exit.Begin < kJumpBytes && exit.First > p) { + const auto j = exit.First - 1; + const auto& prior = body[j]; + if (prior.Offset + prior.Length != exit.Begin || prior.Info.FlowKind != ControlFlowKind::Sequential || prior.Info.HasRipRelativeDisp || frees.contains(j) || pushesOrPops(at(j), prior)) break; + const auto priorUse = memoryUse(at(j), prior); + const auto bytes = priorUse.RspBased ? withDisplacement(at(j), prior, priorUse, priorUse.RspDisp + shift) : std::vector(at(j), at(j) + prior.Length); + if (!bytes) break; + exit.Lead.insert(exit.Lead.begin(), bytes->begin(), bytes->end()); + if (priorUse.RspBased) ++exit.Operands; + exit.Begin = prior.Offset; + exit.First = j; + } + if (exit.End - exit.Begin < kJumpBytes) { reason = "exit shorter than a jump"; continue; } + if (targetInside(exit.Begin, exit.End)) { reason = exit.First == exit.Tail ? "branch into an exit" : "branch into the instructions before a short exit"; continue; } + exits.push_back(std::move(exit)); } } if (!leaf || !redZone) continue; ++report.RedZoneFunctions; const auto prologueEnd = p > 0 ? body[p - 1].Offset + body[p - 1].Length : start; - if (reason == nullptr && ((pushed.empty() && allocation == 0) || prologueEnd - start < kJumpBytes)) reason = "prologue shorter than a jump"; - if (reason == nullptr && targetInside(start, prologueEnd)) reason = "branch into the prologue"; + auto entryEnd = prologueEnd; + auto entryLast = p; + std::vector entryTail; + std::size_t entryOperands = 0; + while (entryEnd - start < kJumpBytes && entryLast < body.size()) { + const auto& covered = body[entryLast]; + if (covered.Offset != entryEnd || covered.Info.FlowKind != ControlFlowKind::Sequential || covered.Info.HasRipRelativeDisp || frees.contains(entryLast) || pushesOrPops(at(entryLast), covered)) break; + if (std::any_of(exits.begin(), exits.end(), [&](const ExitSite& exit) { return covered.Offset < exit.End && exit.Begin < covered.Offset + covered.Length; })) break; + const auto use = memoryUse(at(entryLast), covered); + const auto bytes = use.RspBased ? withDisplacement(at(entryLast), covered, use, use.RspDisp + shift) : std::vector(at(entryLast), at(entryLast) + covered.Length); + if (!bytes) break; + entryTail.insert(entryTail.end(), bytes->begin(), bytes->end()); + if (use.RspBased) ++entryOperands; + entryEnd += covered.Length; + ++entryLast; + } + if (reason == nullptr && entryEnd - start < kJumpBytes) reason = "prologue shorter than a jump"; + if (reason == nullptr && targetInside(start, entryEnd)) reason = entryEnd == prologueEnd ? "branch into the prologue" : "branch into the instructions after a short prologue"; + if (reason == nullptr && entryEnd != prologueEnd && targets.contains(start)) reason = "branch to the start of a widened prologue"; if (reason == nullptr && exits.empty()) reason = "no ret exit"; - if (reason == nullptr && (overlapsExisting(start, prologueEnd) || std::any_of(exits.begin(), exits.end(), [&](const auto& exit) { return overlapsExisting(exit.first, exit.second); }))) reason = "overlaps another stub"; + if (reason == nullptr && std::any_of(exits.begin(), exits.end(), [&](const ExitSite& exit) { return exit.Begin < entryEnd; })) reason = "prologue shorter than a jump"; + if (reason == nullptr && (overlapsExisting(start, entryEnd) || std::any_of(exits.begin(), exits.end(), [&](const ExitSite& exit) { return overlapsExisting(exit.Begin, exit.End); }))) reason = "overlaps another stub"; + const auto carried = [&](const std::size_t k) { + moves.erase(k); + stubbed.erase(std::remove(stubbed.begin(), stubbed.end(), k), stubbed.end()); + }; + for (auto k = p; k < entryLast; ++k) carried(k); + for (const auto& exit : exits) + for (auto k = exit.First; k < exit.Tail; ++k) carried(k); std::vector stubs; const auto overlapsSite = [&](const std::size_t begin, const std::size_t end) { - if (begin < prologueEnd && start < end) return true; - if (std::any_of(exits.begin(), exits.end(), [&](const auto& exit) { return begin < exit.second && exit.first < end; })) return true; + if (begin < entryEnd && start < end) return true; + if (std::any_of(exits.begin(), exits.end(), [&](const ExitSite& exit) { return begin < exit.End && exit.Begin < end; })) return true; return std::any_of(stubs.begin(), stubs.end(), [&](const OperandStub& stub) { return begin < stub.End && stub.Begin < end; }); }; for (const auto k : stubbed) { @@ -405,18 +467,27 @@ void frameSegment( auto entry = original(start, prologueEnd); const auto lower = lowerRsp(shift); entry.insert(entry.end(), lower.begin(), lower.end()); - addSite(start, prologueEnd, std::move(entry)); - for (const auto& exit : exits) { - auto code = raiseRsp(shift); - const auto tail = original(exit.first, exit.second); + entry.insert(entry.end(), entryTail.begin(), entryTail.end()); + addSite(start, entryEnd, std::move(entry)); + for (auto& exit : exits) { + auto code = std::move(exit.Lead); + const auto raise = raiseRsp(shift); + code.insert(code.end(), raise.begin(), raise.end()); + const auto tail = original(exit.TailBegin, exit.End); code.insert(code.end(), tail.begin(), tail.end()); - addSite(exit.first, exit.second, std::move(code)); + addSite(exit.Begin, exit.End, std::move(code)); } for (auto& stub : stubs) addSite(stub.Begin, stub.End, std::move(stub.Body)); for (auto& [k, move] : moves) displacements.push_back({ph.Offset + move.first, std::move(move.second)}); ++report.Framed; if (!rbpPrologue) ++report.FramedWithoutFramePointer; report.Exits += exits.size(); + if (entryLast != p) ++report.WidenedPrologues; + report.WidenedOperands += entryOperands; + for (const auto& exit : exits) { + if (exit.First != exit.Tail) ++report.WidenedExits; + report.WidenedOperands += exit.Operands; + } report.MovedOperands += moves.size(); report.OperandStubs += stubs.size(); for (const auto& stub : stubs) report.StubbedOperands += stub.Operands; 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/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 274b054ad6..6c4d676b52 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -1022,6 +1022,32 @@ void converterClaimedFollower() { } } +void converterWideStoreIslandAcrossInt64Max() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + Bytes text = {0xE8, 0x00, 0x00, 0x00, 0x00, 0xC5, 0xFC, 0x11, 0x07, 0xC3, 0xEB, 0xFD}; + for (int index = 0; index < 4; ++index) + text.insert(text.end(), {0x48, 0x83, 0xC1, 0x01}); + const auto convertAt = [&](const std::uint64_t base) { + Bytes file(0x300 + text.size(), 0xCC); + std::copy(text.begin(), text.end(), file.begin() + 0x200); + return converter->Convert(file, {{1, 5, 0x200, base, 0, text.size(), text.size(), 16}}); + }; + const auto low = convertAt(0x1000); + const auto high = convertAt(0x8000000000000000 - 0x10); + require(high.Trampolines.size() == 2 && high.KeptCount == 0, "A 256-bit store just below 2^63 was not lowered through an island above it"); + require(high.Bytes == low.Bytes, "An island across 2^63 changed the segment bytes"); + for (std::size_t index = 0; index < 2; ++index) { + const auto& site = high.Trampolines[index]; + const auto& reference = low.Trampolines[index]; + require(site.Offset == reference.Offset && site.Length == reference.Length && site.Body == reference.Body, "An island across 2^63 changed a trampoline"); + require(site.Relocations.size() == reference.Relocations.size(), "An island across 2^63 changed the number of relocations"); + for (std::size_t relocation = 0; relocation < site.Relocations.size(); ++relocation) + require(site.Relocations[relocation].DisplacementOffset == reference.Relocations[relocation].DisplacementOffset && + site.Relocations[relocation].SiteTarget == reference.Relocations[relocation].SiteTarget, "An island across 2^63 changed where a stub jumps"); + } + require(Bytes(high.Bytes.begin() + 0x205, high.Bytes.begin() + 0x207) == Bytes{0xEB, 0x0D}, "The short jump to an island across 2^63 has the wrong displacement"); +} + void converterJumpTableTargets() { const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); const Bytes leaTable = {0x48, 0x8D, 0x0D, 0xF9, 0x1F, 0x00, 0x00}; @@ -2120,6 +2146,91 @@ void scalarCompareExecution() { } 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() {} @@ -2167,6 +2278,9 @@ void rdtscpExecution() { void rdtscpExecution() {} void wideStoreExecution() {} void scalarCompareExecution() {} +void rdpidExecution() {} +void rdseedExecution() {} +void clwbExecution() {} #endif void scannerZeroTail() { @@ -2229,8 +2343,12 @@ int main() { rdtscpExecution(); wideStoreExecution(); scalarCompareExecution(); + rdpidExecution(); + rdseedExecution(); + clwbExecution(); wideStoreSplit(); converterWideStoreWindows(); + converterWideStoreIslandAcrossInt64Max(); converterClaimedFollower(); converterJumpTableTargets(); converterReciprocal(); 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/RedZoneFramerExecutionTests.cpp b/core/relinker/codegen/tests/RedZoneFramerExecutionTests.cpp index 7d940524b9..d7f47026dd 100644 --- a/core/relinker/codegen/tests/RedZoneFramerExecutionTests.cpp +++ b/core/relinker/codegen/tests/RedZoneFramerExecutionTests.cpp @@ -1,6 +1,12 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#endif #include #include #include +#include #include #include #include @@ -16,7 +22,7 @@ #include #endif -#if defined(__linux__) && defined(__x86_64__) +#if (defined(__linux__) || defined(_WIN32)) && (defined(__x86_64__) || defined(_M_X64)) namespace { @@ -31,7 +37,36 @@ constexpr std::uint8_t kClobber = 0xa5; constexpr std::uint64_t kA = 0x1111, kB = 0x2222, kC = 0x3333, kD = 0x4444; std::uint8_t* arena = nullptr; -volatile sig_atomic_t steps = 0; +volatile std::sig_atomic_t steps = 0; +volatile std::sig_atomic_t outsideSteps = 0; + +#ifdef _WIN32 +DWORD steppingThread = 0; +bool singleStepping = false; +bool steppingFinished = false; +std::uintptr_t steppingReturn = 0; + +LONG CALLBACK clobberBelowRsp(EXCEPTION_POINTERS* exception) { + if (GetCurrentThreadId() != steppingThread || !singleStepping || exception->ExceptionRecord->ExceptionCode != EXCEPTION_SINGLE_STEP) + return EXCEPTION_CONTINUE_SEARCH; + auto& registers = *exception->ContextRecord; + if (registers.Rip == steppingReturn) { + registers.EFlags &= ~0x100u; + steppingFinished = true; + return EXCEPTION_CONTINUE_EXECUTION; + } + const auto base = reinterpret_cast(arena); + if (registers.Rip < base || registers.Rip >= base + kArenaBytes) { + registers.EFlags &= ~0x100u; + outsideSteps = outsideSteps + 1; + return EXCEPTION_CONTINUE_EXECUTION; + } + std::memset(reinterpret_cast(registers.Rsp - 0x80), kClobber, 0x80); + registers.EFlags |= 0x100u; + steps = steps + 1; + return EXCEPTION_CONTINUE_EXECUTION; +} +#else void clobberBelowRsp(int, siginfo_t*, void* context) { auto& registers = static_cast(context)->uc_mcontext.gregs; @@ -39,18 +74,115 @@ void clobberBelowRsp(int, siginfo_t*, void* context) { const auto base = reinterpret_cast(arena); if (rip < base || rip >= base + kArenaBytes) { registers[REG_EFL] &= ~static_cast(0x100); + outsideSteps = outsideSteps + 1; return; } std::memset(reinterpret_cast(static_cast(registers[REG_RSP]) - 0x80), kClobber, 0x80); steps = steps + 1; } +#endif -const std::vector Runner{ +class ExecutableArena { +public: + ExecutableArena() { +#ifdef _WIN32 + arena = static_cast(VirtualAlloc(nullptr, kArenaBytes, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE)); +#else + const auto mapped = mmap(nullptr, kArenaBytes, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + arena = mapped == MAP_FAILED ? nullptr : static_cast(mapped); +#endif + } + + ~ExecutableArena() { + if (arena == nullptr) return; +#ifdef _WIN32 + VirtualFree(arena, 0, MEM_RELEASE); +#else + munmap(arena, kArenaBytes); +#endif + arena = nullptr; + } + + ExecutableArena(const ExecutableArena&) = delete; + ExecutableArena& operator=(const ExecutableArena&) = delete; +}; + +class SingleStepHandler { +public: + SingleStepHandler() { +#ifdef _WIN32 + steppingThread = GetCurrentThreadId(); + handler = AddVectoredExceptionHandler(1, clobberBelowRsp); + require(handler != nullptr, "cannot install the single-step handler"); +#else + struct sigaction action {}; + action.sa_sigaction = clobberBelowRsp; + action.sa_flags = SA_SIGINFO; + sigemptyset(&action.sa_mask); + require(sigaction(SIGTRAP, &action, &previous) == 0, "cannot install the SIGTRAP handler"); +#endif + } + + ~SingleStepHandler() { +#ifdef _WIN32 + singleStepping = false; + RemoveVectoredExceptionHandler(handler); +#else + sigaction(SIGTRAP, &previous, nullptr); +#endif + } + + SingleStepHandler(const SingleStepHandler&) = delete; + SingleStepHandler& operator=(const SingleStepHandler&) = delete; + +private: +#ifdef _WIN32 + PVOID handler = nullptr; +#else + struct sigaction previous {}; +#endif +}; + +const std::vector RunnerEntry{ 0x53, 0x4c, 0x89, 0xc3, 0x48, 0x89, 0xf8, 0x48, 0x89, 0xf7, 0x48, 0x89, 0xd6, 0x48, 0x89, 0xca, 0x4d, 0x85, 0xc9, 0x74, 0x0a, 0x9c, 0x48, 0x81, 0x0c, 0x24, 0x00, 0x01, 0x00, 0x00, 0x9d, - 0xff, 0xd0, 0x9c, 0x48, 0x81, 0x24, 0x24, 0xff, 0xfe, 0xff, 0xff, 0x9d, 0x5b, 0xc3, + 0xff, 0xd0, }; +const std::vector Runner = [] { + auto bytes = RunnerEntry; + bytes.insert(bytes.end(), {0x9c, 0x48, 0x81, 0x24, 0x24, 0xff, 0xfe, 0xff, 0xff, 0x9d, 0x5b, 0xc3}); + return bytes; +}(); + +#ifdef _WIN32 +std::vector windowsRunner() { + std::vector bytes{0x57, 0x56, 0x48, 0x81, 0xec, 0xa8, 0x00, 0x00, 0x00}; + const auto transferXmm = [&](const std::uint8_t opcode) { + for (std::uint8_t reg = 6; reg < 16; ++reg) { + bytes.push_back(0xf3); + if (reg >= 8) bytes.push_back(0x44); + bytes.insert(bytes.end(), {0x0f, opcode, static_cast(0x84 | ((reg & 7) << 3)), 0x24, + static_cast((reg - 6) * 16), 0x00, 0x00, 0x00}); + } + }; + transferXmm(0x7f); + bytes.insert(bytes.end(), { + 0x48, 0x89, 0xcf, 0x48, 0x89, 0xd6, 0x4c, 0x89, 0xc2, 0x4c, 0x89, 0xc9, + 0x4c, 0x8b, 0x84, 0x24, 0xe0, 0x00, 0x00, 0x00, + 0x4c, 0x8b, 0x8c, 0x24, 0xe8, 0x00, 0x00, 0x00, + }); + const auto call = bytes.size(); + bytes.insert(bytes.end(), {0xe8, 0x00, 0x00, 0x00, 0x00}); + transferXmm(0x6f); + bytes.insert(bytes.end(), {0x48, 0x81, 0xc4, 0xa8, 0x00, 0x00, 0x00, 0x5e, 0x5f, 0xc3}); + const auto displacement = static_cast(bytes.size() - call - 5); + for (std::size_t index = 0; index < 4; ++index) bytes[call + 1 + index] = static_cast(displacement >> (index * 8)); + bytes.insert(bytes.end(), Runner.begin(), Runner.end()); + return bytes; +} +#endif + struct Leaf { const char* Name; std::vector Code; @@ -127,6 +259,45 @@ std::vector backwardStubbedLeaves() { }; } +std::vector widenedLeaves() { + return { + {"leaf without pushes spilling 0x10 below rsp and adding a spill before its ret", { + 0x48, 0x89, 0x7c, 0x24, 0xf0, 0x48, 0x89, 0x74, 0x24, 0xf8, 0x31, 0xc0, 0xb9, 0x08, 0x00, 0x00, 0x00, + 0x48, 0x03, 0x44, 0x24, 0xf0, 0x48, 0x03, 0x44, 0x24, 0xf8, 0xff, 0xc9, 0x75, 0xf2, + 0x48, 0x03, 0x44, 0x24, 0xf0, 0xc3, + }, 8 * (kA + kB) + kA}, + {"leaf pushing only rbx, spilling below rsp and adding rbx before its pop", { + 0x53, 0x48, 0x89, 0x7c, 0x24, 0xf0, 0x48, 0x89, 0xf3, 0x31, 0xc0, 0xb9, 0x08, 0x00, 0x00, 0x00, + 0x48, 0x03, 0x44, 0x24, 0xf0, 0x48, 0x01, 0xd8, 0xff, 0xc9, 0x75, 0xf4, + 0x48, 0x01, 0xd8, 0x5b, 0xc3, + }, 8 * (kA + kB) + kB}, + {"frame-pointer leaf without other pushes spilling below rbp", { + 0x55, 0x48, 0x89, 0xe5, 0x48, 0x89, 0x7d, 0xf0, 0x48, 0x89, 0x75, 0xf8, 0x31, 0xc0, 0xb9, 0x08, 0x00, 0x00, 0x00, + 0x48, 0x03, 0x45, 0xf0, 0x48, 0x03, 0x45, 0xf8, 0xff, 0xc9, 0x75, 0xf4, + 0x48, 0x03, 0x45, 0xf0, 0x5d, 0xc3, + }, 8 * (kA + kB) + kA}, + {"leaf without pushes returning its first spill through the first of two widened exits", { + 0x48, 0x89, 0x7c, 0x24, 0xf0, 0x48, 0x89, 0x74, 0x24, 0xf8, 0x48, 0x39, 0xf7, 0x74, 0x06, + 0x48, 0x8b, 0x44, 0x24, 0xf0, 0xc3, 0x48, 0x8b, 0x44, 0x24, 0xf8, 0xc3, + }, kA}, + {"leaf without pushes returning its second spill through the widened exit a jne reaches", { + 0x48, 0x89, 0x7c, 0x24, 0xf0, 0x48, 0x89, 0x74, 0x24, 0xf8, 0x48, 0x39, 0xf7, 0x75, 0x06, + 0x48, 0x8b, 0x44, 0x24, 0xf0, 0xc3, 0x48, 0x8b, 0x44, 0x24, 0xf8, 0xc3, + }, kB}, + {"leaf without pushes whose widened exit carries a VEX andn below rsp", { + 0x48, 0x89, 0x7c, 0x24, 0xf8, 0xc4, 0xe2, 0xf0, 0xf2, 0x44, 0x24, 0xf8, 0xc3, + }, ~kC & kA}, + {"leaf allocating with a 4-byte sub widened over the spill after it", { + 0x48, 0x83, 0xec, 0x08, 0x48, 0x89, 0x7c, 0x24, 0xf0, 0x31, 0xc0, 0x48, 0x03, 0x44, 0x24, 0xf0, + 0x48, 0x83, 0xc4, 0x08, 0xc3, + }, kA}, + {"frame-pointer leaf pushing and popping rbx in its body below a slot at rbp-0x10", { + 0x55, 0x48, 0x89, 0xe5, 0x48, 0x89, 0x7d, 0xf0, 0x53, 0x48, 0x89, 0xf3, 0x48, 0x8b, 0x45, 0xf0, + 0x48, 0x01, 0xd8, 0x5b, 0x48, 0x01, 0xd0, 0x5d, 0xc3, + }, kA + kB + kC}, + }; +} + void writeRel32(std::uint8_t* at, const std::uint64_t target, const std::uint64_t next) { const auto displacement = static_cast(target) - static_cast(next); require(displacement >= std::numeric_limits::min() && displacement <= std::numeric_limits::max(), "a stub jump is out of rel32 range"); @@ -178,7 +349,14 @@ struct Framing { Framing load(const Image& image, const bool frame) { std::fill_n(arena, kArenaBytes, 0xcc); std::copy(image.Bytes.begin(), image.Bytes.end(), arena); - std::copy(Runner.begin(), Runner.end(), arena + kRunnerBase); +#ifdef _WIN32 + const auto runner = windowsRunner(); + steppingReturn = reinterpret_cast(arena + kRunnerBase + runner.size() - Runner.size() + RunnerEntry.size()); +#else + const auto& runner = Runner; +#endif + require(runner.size() <= kArenaBytes - kRunnerBase, "the runner does not fit in the executable arena"); + std::copy(runner.begin(), runner.end(), arena + kRunnerBase); Framing framing; if (!frame) return framing; const auto base = reinterpret_cast(arena); @@ -214,7 +392,19 @@ Run run(const std::size_t start, const bool clobber) { using Entry = std::uint64_t (*)(std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t); const auto entry = reinterpret_cast(arena + kRunnerBase); steps = 0; + outsideSteps = 0; +#ifdef _WIN32 + require(FlushInstructionCache(GetCurrentProcess(), arena, kArenaBytes) != 0, "cannot flush the generated code"); + singleStepping = clobber; + steppingFinished = false; +#endif const auto result = entry(reinterpret_cast(arena + start), kA, kB, kC, kD, clobber ? 1 : 0); +#ifdef _WIN32 + singleStepping = false; + require(steppingFinished == clobber, "single-stepping did not finish at the runner's return point"); +#endif + require(outsideSteps == 0, "single-stepping escaped the generated code"); + require(clobber || steps == 0, "single-stepping ran without the clobber requested"); return {result, static_cast(steps)}; } @@ -252,22 +442,19 @@ Codegen::RedZoneFrameReport runLeaves(const std::vector& leaves) { int main() { try { - struct sigaction action {}; - action.sa_sigaction = clobberBelowRsp; - action.sa_flags = SA_SIGINFO; - sigemptyset(&action.sa_mask); - require(sigaction(SIGTRAP, &action, nullptr) == 0, "cannot install the SIGTRAP handler"); - void* mapped = mmap(nullptr, kArenaBytes, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); - if (mapped == MAP_FAILED) { + const ExecutableArena executableArena; + if (arena == nullptr) { std::cout << "skipped: no writable and executable memory\n"; return 77; } - arena = static_cast(mapped); + const SingleStepHandler singleStepHandler; runLeaves(framedLeaves()); const auto stubbed = runLeaves(stubbedLeaves()); require(stubbed.OperandStubs == 3 && stubbed.StubbedOperands == 5, "the leaves were framed with " + std::to_string(stubbed.OperandStubs) + " operand stubs carrying " + std::to_string(stubbed.StubbedOperands) + " rsp operands, not 3 carrying 5"); const auto backward = runLeaves(backwardStubbedLeaves()); require(backward.OperandStubs == 2 && backward.StubbedOperands == 3 && backward.MovedOperands == 5, "the leaves comparing at rsp before a branch were framed with " + std::to_string(backward.OperandStubs) + " operand stubs carrying " + std::to_string(backward.StubbedOperands) + " rsp operands and " + std::to_string(backward.MovedOperands) + " moved in place, not 2 carrying 3 and 5"); + const auto widened = runLeaves(widenedLeaves()); + require(widened.WidenedPrologues == 8 && widened.WidenedExits == 9 && widened.WidenedOperands == 12, "the leaves with short prologues and exits were framed with " + std::to_string(widened.WidenedPrologues) + " widened prologues, " + std::to_string(widened.WidenedExits) + " widened exits and " + std::to_string(widened.WidenedOperands) + " rsp operands in them, not 8, 9 and 12"); std::cout << "red-zone framer execution tests passed\n"; return 0; } catch (const std::exception& error) { @@ -279,7 +466,7 @@ int main() { #else int main() { - std::cout << "skipped: the red-zone framer execution tests single-step x86-64 code on Linux\n"; + std::cout << "skipped: the red-zone framer execution tests require x86-64 Linux or Windows\n"; return 77; } diff --git a/core/relinker/codegen/tests/RedZoneFramerTests.cpp b/core/relinker/codegen/tests/RedZoneFramerTests.cpp index 31466fcfdb..f6a4e5c0fc 100644 --- a/core/relinker/codegen/tests/RedZoneFramerTests.cpp +++ b/core/relinker/codegen/tests/RedZoneFramerTests.cpp @@ -275,14 +275,190 @@ void skipsLeavesWithOperandsThatCannotMove() { std::vector beforeTarget = StoreBelowRsp; append(beforeTarget, LoadAtRsp); append(beforeTarget, {0xff, 0xc0, 0x3b, 0x44, 0x24, 0xf0, 0x72, 0xf8}); - const auto input = image({framelessLeaf(FramelessPrologue, evex, FramelessExit), framelessLeaf(FramelessPrologue, beforeExit, FramelessExit), framelessLeaf(FramelessPrologue, beforeTarget, FramelessExit), framelessLeaf({0x41, 0x56, 0x41, 0x57}, StoreBelowRsp, {0x41, 0x5f, 0x41, 0x5e, 0xc3})}); + std::vector afterShortPrologue = LoadRipRelative; + append(afterShortPrologue, StoreBelowRsp); + const auto input = image({framelessLeaf(FramelessPrologue, evex, FramelessExit), framelessLeaf(FramelessPrologue, beforeExit, FramelessExit), framelessLeaf(FramelessPrologue, beforeTarget, FramelessExit), framelessLeaf({0x41, 0x56, 0x41, 0x57}, afterShortPrologue, {0x41, 0x5f, 0x41, 0x5e, 0xc3})}); Codegen::RedZoneFrameReport report; std::vector displacements; const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); require(sites.empty() && displacements.empty() && report.RedZoneFunctions == 4, "a leaf whose rsp operands can neither move nor be stubbed, or whose prologue is too short, was framed"); require(report.Skipped.at("rsp operand that cannot move with rsp") == 1, "an EVEX operand, whose 8-bit displacement is scaled, was not refused"); require(report.Skipped.at("rsp operand that cannot move and has no room for a stub") == 2, "an operand at rsp followed by an exit or a branch target was not refused for want of room"); - require(report.Skipped.at("prologue shorter than a jump") == 1, "the leaf with a short prologue was not reported with its reason"); + require(report.Skipped.at("prologue shorter than a jump") == 1, "the leaf whose short prologue is followed by a rip-relative instruction was not reported with its reason"); +} + +const std::vector PushRbx{0x53}; +const std::vector PopRbxExit{0x5b, 0xc3}; +const std::vector Ret{0xc3}; + +void widensShortProloguesAndExits() { + std::vector body = StoreBelowRsp; + append(body, LoadBelowRsp); + append(body, AddRdxToRax); + const auto input = image({framelessLeaf(PushRbx, body, PopRbxExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 1 && report.WidenedPrologues == 1 && report.WidenedExits == 1 && report.WidenedOperands == 1 && report.MovedOperands == 1 && sites.size() == 2 && report.Skipped.empty(), "a leaf with a 1-byte push and a 2-byte exit was not framed by widening both sites"); + const auto& entry = sites[0]; + require(entry.Address == Base + 0x40 && entry.Length == PushRbx.size() + StoreBelowRsp.size(), "the prologue site does not run on over the store after the push"); + require(entry.Body == stubBody({0x53, 0x48, 0x8d, 0x64, 0x24, 0xf0, 0x48, 0x89, 0x7c, 0x24, 0x00}) && entry.ReturnBranchOffset == 11, "the store after the push was not moved by the shift after rsp is lowered"); + const auto& exit = sites[1]; + const auto add = Base + 0x40 + PushRbx.size() + StoreBelowRsp.size() + LoadBelowRsp.size(); + require(exit.Address == add && exit.Length == AddRdxToRax.size() + PopRbxExit.size(), "the exit site does not start at the add before the pop"); + std::vector exitBody = AddRdxToRax; + append(exitBody, {0x48, 0x8d, 0xa4, 0x24, 0x10, 0x00, 0x00, 0x00}); + append(exitBody, PopRbxExit); + require(exit.Body == stubBody(exitBody), "the add was not copied ahead of raising rsp and the pop"); + require(displacements.size() == 1 && displacements[0].Offset == 0x40 + PushRbx.size() + StoreBelowRsp.size() + 4 && displacements[0].Bytes == std::vector{0x00}, "the load between the widened sites was not moved in place"); +} + +void widensAShortFramePointerPrologue() { + const std::vector framePrologue{0x55, 0x48, 0x89, 0xe5}; + const std::vector popRbpExit{0x5d, 0xc3}; + std::vector body = StoreBelowPushes; + append(body, LoadBelowPushes); + const auto input = image({framelessLeaf(framePrologue, body, popRbpExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 1 && report.FramedWithoutFramePointer == 0 && report.WidenedPrologues == 1 && report.WidenedExits == 1 && report.WidenedOperands == 0 && sites.size() == 2 && displacements.empty(), "a frame-pointer leaf without other pushes was not framed by widening its 4-byte prologue and 2-byte exit"); + std::vector entryBody = framePrologue; + append(entryBody, {0x48, 0x8d, 0x64, 0x24, 0x80}); + append(entryBody, StoreBelowPushes); + require(sites[0].OriginalBytes.size() == framePrologue.size() + StoreBelowPushes.size() && sites[0].Body == stubBody(entryBody), "the store through rbp was not copied unchanged after rsp is lowered by the whole red zone"); + std::vector exitBody = LoadBelowPushes; + append(exitBody, {0x48, 0x8d, 0xa4, 0x24, 0x80, 0x00, 0x00, 0x00}); + append(exitBody, popRbpExit); + require(sites[1].OriginalBytes.size() == LoadBelowPushes.size() + popRbpExit.size() && sites[1].Body == stubBody(exitBody), "the load through rbp was not copied unchanged ahead of raising rsp and popping rbp"); +} + +void framesLeavesWithoutPushes() { + std::vector body = StoreBelowRsp; + append(body, LoadBelowRsp); + const auto input = image({framelessLeaf({}, body, Ret)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 1 && report.WidenedPrologues == 1 && report.WidenedExits == 1 && report.WidenedOperands == 2 && sites.size() == 2 && displacements.empty(), "a leaf that only spills below rsp and returns was not framed"); + require(sites[0].Address == Base + 0x40 && sites[0].OriginalBytes == StoreBelowRsp, "the entry site of a leaf without pushes does not cover its first instruction"); + require(sites[0].Body == stubBody({0x48, 0x8d, 0x64, 0x24, 0xf0, 0x48, 0x89, 0x7c, 0x24, 0x00}), "the entry stub does not lower rsp before the store moved by the shift"); + std::vector exitBody{0x48, 0x8b, 0x44, 0x24, 0x00, 0x48, 0x8d, 0xa4, 0x24, 0x10, 0x00, 0x00, 0x00}; + append(exitBody, Ret); + require(sites[1].OriginalBytes.size() == LoadBelowRsp.size() + Ret.size() && sites[1].Body == stubBody(exitBody), "the exit stub does not run the load moved by the shift, raise rsp and return"); +} + +void skipsFragmentsThatPopWithoutPushing() { + std::vector body = StoreBelowRsp; + append(body, LoadBelowRsp); + const auto input = image({framelessLeaf({}, body, PopRbxExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(sites.empty() && displacements.empty() && report.Skipped.at("push or pop in a leaf without pushes") == 1, "a fragment without pushes that pops a register its caller pushed was framed"); +} + +void skipsWideningOverBranchTargets() { + const std::vector loopToStart{0x48, 0x89, 0x7c, 0x24, 0xf0, 0xff, 0xc9, 0x75, 0xf7, 0x48, 0x8b, 0x44, 0x24, 0xf0, 0xc3}; + std::vector jumpToPop = StoreBelowRsp; + append(jumpToPop, {0x85, 0xff, 0x74, 0x03}); + append(jumpToPop, AddRdxToRax); + const auto input = image({loopToStart, framelessLeaf(PushRbx, jumpToPop, PopRbxExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(sites.empty() && displacements.empty() && report.RedZoneFunctions == 2, "a leaf with a branch into a widened site was framed"); + require(report.Skipped.at("branch to the start of a widened prologue") == 1, "a leaf without pushes whose loop branches back to its first instruction was not refused"); + require(report.Skipped.at("branch into the instructions before a short exit") == 1, "a short exit that a branch reaches past the add before it was not refused"); +} + +void skipsBranchesIntoTheInstructionsAfterAShortPrologue() { + std::vector loopToStore = StoreBelowRsp; + append(loopToStore, {0xff, 0xc9, 0x75, 0xf7}); + append(loopToStore, LoadBelowRsp); + const auto input = image({framelessLeaf(PushRbx, loopToStore, PopRbxExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(sites.empty() && displacements.empty() && report.RedZoneFunctions == 1, "a leaf whose loop branches to the store the widened prologue borrows was framed"); + require(report.Skipped.at("branch into the instructions after a short prologue") == 1, "a loop to the instruction after a 1-byte push was not refused with its reason"); +} + +void widensAShortPrologueThatAllocates() { + const std::vector allocate{0x48, 0x83, 0xec, 0x08}; + const std::vector freeExit{0x48, 0x83, 0xc4, 0x08, 0xc3}; + std::vector body = StoreBelowRsp; + append(body, {0x31, 0xc0, 0x48, 0x03, 0x44, 0x24, 0xf0}); + const auto input = image({framelessLeaf(allocate, body, freeExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 1 && report.FramedWithoutFramePointer == 1 && report.WidenedPrologues == 1 && report.WidenedExits == 0 && report.WidenedOperands == 1 && report.MovedOperands == 1 && sites.size() == 2 && report.Skipped.empty(), "a leaf whose 4-byte sub is its whole prologue was not framed by widening the prologue over the store after it"); + std::vector entryBody = allocate; + append(entryBody, {0x48, 0x8d, 0x64, 0x24, 0xf0, 0x48, 0x89, 0x7c, 0x24, 0x00}); + require(sites[0].Address == Base + 0x40 && sites[0].OriginalBytes.size() == allocate.size() + StoreBelowRsp.size() && sites[0].Body == stubBody(entryBody), "the store after the sub was not moved by the shift after the sub and the lowering lea"); + std::vector exitBody{0x48, 0x8d, 0xa4, 0x24, 0x10, 0x00, 0x00, 0x00}; + append(exitBody, freeExit); + require(sites[1].OriginalBytes == freeExit && sites[1].Body == stubBody(exitBody), "rsp is not raised by the shift before the add that frees the allocation"); + require(displacements.size() == 1 && displacements[0].Offset == 0x40 + allocate.size() + StoreBelowRsp.size() + 2 + 4 && displacements[0].Bytes == std::vector{0x00}, "the load between the widened prologue and the exit was not moved in place"); +} + +void widensBothExitsOfALeafWithoutPushes() { + const auto twoExits = [](const std::uint8_t branch) { + std::vector bytes = StoreBelowRsp; + append(bytes, {0x48, 0x89, 0x74, 0x24, 0xf8, 0x48, 0x39, 0xf7, branch, 0x06}); + append(bytes, LoadBelowRsp); + append(bytes, Ret); + append(bytes, {0x48, 0x8b, 0x44, 0x24, 0xf8}); + append(bytes, Ret); + return bytes; + }; + const auto input = image({twoExits(0x74), twoExits(0x75)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 2 && report.Exits == 4 && report.WidenedPrologues == 2 && report.WidenedExits == 4 && report.WidenedOperands == 6 && report.MovedOperands == 2 && sites.size() == 6 && report.Skipped.empty(), "leaves without pushes that return from two places were not framed with a widened prologue and two widened exits"); + require(sites[0].Body == stubBody({0x48, 0x8d, 0x64, 0x24, 0xf0, 0x48, 0x89, 0x7c, 0x24, 0x00}), "the entry stub does not lower rsp before the store moved by the shift"); + std::vector firstExit{0x48, 0x8b, 0x44, 0x24, 0x00, 0x48, 0x8d, 0xa4, 0x24, 0x10, 0x00, 0x00, 0x00}; + append(firstExit, Ret); + std::vector secondExit{0x48, 0x8b, 0x44, 0x24, 0x08, 0x48, 0x8d, 0xa4, 0x24, 0x10, 0x00, 0x00, 0x00}; + append(secondExit, Ret); + require(sites[1].Address == Base + 0x40 + 15 && sites[1].Body == stubBody(firstExit), "the first ret was not widened over the load before it, moved by the shift"); + require(sites[2].Address == Base + 0x40 + 21 && sites[2].Body == stubBody(secondExit), "the ret a branch reaches at its load was not widened over that load, moved by the shift"); + require(displacements[0].Offset == 0x40 + 9 && displacements[0].Bytes == std::vector{0x08}, "the second spill was not moved in place"); +} + +void widensExitsOverVexOperandsAtRsp() { + const std::vector andnBelowRsp{0xc4, 0xe2, 0xf0, 0xf2, 0x44, 0x24, 0xf8}; + std::vector body{0x48, 0x89, 0x7c, 0x24, 0xf8}; + append(body, andnBelowRsp); + const auto input = image({framelessLeaf({}, body, Ret)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 1 && report.WidenedPrologues == 1 && report.WidenedExits == 1 && report.WidenedOperands == 2 && sites.size() == 2 && displacements.empty(), "a leaf whose ret follows a VEX andn below rsp was not framed by widening its exit over the andn"); + require(sites[0].Body == stubBody({0x48, 0x8d, 0x64, 0x24, 0xf0, 0x48, 0x89, 0x7c, 0x24, 0x08}), "the spill was not moved by the shift after rsp is lowered"); + std::vector exitBody{0xc4, 0xe2, 0xf0, 0xf2, 0x44, 0x24, 0x08, 0x48, 0x8d, 0xa4, 0x24, 0x10, 0x00, 0x00, 0x00}; + append(exitBody, Ret); + require(sites[1].OriginalBytes.size() == andnBelowRsp.size() + Ret.size() && sites[1].Body == stubBody(exitBody), "the VEX andn was not re-encoded with its displacement moved by the shift ahead of raising rsp"); +} + +void framesFramePointerLeavesThatPushInTheirBody() { + const std::vector framePrologue{0x55, 0x48, 0x89, 0xe5}; + std::vector body = StoreBelowPushes; + append(body, {0x53, 0x48, 0x89, 0xf3}); + append(body, LoadBelowPushes); + append(body, {0x48, 0x01, 0xd8, 0x5b, 0x48, 0x01, 0xd0}); + const auto input = image({framelessLeaf(framePrologue, body, {0x5d, 0xc3})}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 1 && report.FramedWithoutFramePointer == 0 && report.WidenedPrologues == 1 && report.WidenedExits == 1 && report.WidenedOperands == 0 && sites.size() == 2 && displacements.empty() && report.Skipped.empty(), "a frame-pointer leaf that pushes and pops rbx in its body was not framed"); + std::vector entryBody = framePrologue; + append(entryBody, {0x48, 0x8d, 0x64, 0x24, 0x80}); + append(entryBody, StoreBelowPushes); + require(sites[0].Body == stubBody(entryBody), "the store through rbp was not copied unchanged after rsp is lowered by the whole red zone"); + require(sites[1].Body == stubBody({0x48, 0x01, 0xd0, 0x48, 0x8d, 0xa4, 0x24, 0x80, 0x00, 0x00, 0x00, 0x5d, 0xc3}), "the add before pop rbp was not copied ahead of raising rsp"); } void skipsLeavesThatCopyRsp() { @@ -297,6 +473,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}; @@ -608,6 +809,16 @@ int main() { stubsShortOperandsAtRspBeforeABranchWithTheInstructionsBeforeThem(); skipsShortOperandsAtRspWhoseWidenedStubCoversABranchTarget(); skipsLeavesWithOperandsThatCannotMove(); + widensShortProloguesAndExits(); + widensAShortFramePointerPrologue(); + framesLeavesWithoutPushes(); + skipsFragmentsThatPopWithoutPushing(); + skipsWideningOverBranchTargets(); + skipsBranchesIntoTheInstructionsAfterAShortPrologue(); + widensAShortPrologueThatAllocates(); + widensBothExitsOfALeafWithoutPushes(); + widensExitsOverVexOperandsAtRsp(); + framesFramePointerLeavesThatPushInTheirBody(); skipsLeavesThatCopyRsp(); framesEachFunctionFromItsOwnFde(); treatsATailCallPastTheFdeEndAsLeaving(); @@ -616,6 +827,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/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/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 2c406e732b..fb493db7f0 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; @@ -96,11 +98,32 @@ std::vector RebasedRelocations(const std::vector& ta return rebased; } +void RequireZeroImportSlotAddends(const std::vector& table) { + for (std::size_t entry = 0; entry < table.size(); entry += kRelaEntrySize) { + const auto type = static_cast(Io::ReadU64(table, entry + 8)); + if ((type == R_X86_64_GLOB_DAT || type == R_X86_64_JUMP_SLOT) && Io::ReadU64(table, entry + 16) != 0) + throw Domain::RelinkerException("GLOB_DAT/JUMP_SLOT relocation has a nonzero addend", Io::ReadU64(table, entry)); + } +} + void RequireFileRange(const std::vector& buf, const Domain::ProgramHeader& load) { if (load.Offset > buf.size() || load.FileSize > buf.size() - load.Offset) 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) { @@ -112,6 +135,11 @@ void SeparateSharedLoadPages(std::vector& buf, std::vector TracedImports(const Domain::SysVDynamicSection& dynSec const auto type = static_cast(info); const auto symbol = info >> 32; const auto target = Io::ReadU64(table, entry); - if ((type != R_X86_64_GLOB_DAT && type != R_X86_64_JUMP_SLOT) || symbol == 0 || Io::ReadU64(table, entry + 16) != 0) + if ((type != R_X86_64_GLOB_DAT && type != R_X86_64_JUMP_SLOT) || symbol == 0) continue; if (type == R_X86_64_GLOB_DAT && !dynSection.FunctionImports.contains(target)) continue; @@ -346,6 +374,8 @@ 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) @@ -354,8 +384,16 @@ std::vector LinuxElfPatcher::Patch( throw Domain::RelinkerException("--trace-imports conflicts with --lazy-binding"); if (_traceImports && std::find(dynSection.NeededLibraries.begin(), dynSection.NeededLibraries.end(), "libc.prx") == dynSection.NeededLibraries.end()) throw Domain::RelinkerException("--trace-imports requires libc.prx among the libraries"); + RequireZeroImportSlotAddends(dynSection.RelaData); + RequireZeroImportSlotAddends(dynSection.RelaPltData); 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 012c397f06..ec473d8028 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -873,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/WindowsPeWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp index 96dfbf91b3..123bc83ef7 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp @@ -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 (fixedBase && imageBase > 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 f923bafd63..bd5ce8a18c 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -235,7 +235,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co 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. 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/io/src/FileReader.cpp b/core/relinker/io/src/FileReader.cpp index 4f012529d3..5cbed8069b 100644 --- a/core/relinker/io/src/FileReader.cpp +++ b/core/relinker/io/src/FileReader.cpp @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include #include #include @@ -17,12 +19,17 @@ std::vector FileReader::ReadPrefix(const std::string& path, const std::ifstream f(native, std::ios::binary); if (!f) throw Domain::RelinkerException("Cannot open file: " + path); - std::vector buf(size); - f.read(reinterpret_cast(buf.data()), static_cast(size)); - const auto count = f.gcount(); - if (f.bad()) - throw Domain::RelinkerException("Cannot read file: " + path); - buf.resize(static_cast(count)); + std::vector buf; + std::array chunk; + while (buf.size() < size) { + const auto requested = std::min(size - buf.size(), chunk.size()); + f.read(reinterpret_cast(chunk.data()), static_cast(requested)); + const auto count = static_cast(f.gcount()); + if (f.bad()) + throw Domain::RelinkerException("Cannot read file: " + path); + buf.insert(buf.end(), chunk.begin(), chunk.begin() + count); + if (count < requested) break; + } return buf; } 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/FileReaderTests.cpp b/core/relinker/io/tests/FileReaderTests.cpp new file mode 100644 index 0000000000..769bb03303 --- /dev/null +++ b/core/relinker/io/tests/FileReaderTests.cpp @@ -0,0 +1,107 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using Bytes = std::vector; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +template +void requireError(const TAction& action, const std::string& message) { + try { + action(); + } catch (const Domain::RelinkerException& error) { + require(error.what() == message, "Unexpected prefix-read error: " + std::string(error.what())); + return; + } + throw std::runtime_error("Invalid prefix read was accepted: " + message); +} + +void prefixes(const std::filesystem::path& path, const Bytes& expected) { + const Io::FileReader reader; + const auto name = Io::Utf8Path(path); + const auto maximum = static_cast(std::numeric_limits::max()); + for (const std::size_t limit : {std::size_t{0}, std::size_t{1}, std::size_t{2}, std::size_t{4095}, + std::size_t{4096}, std::size_t{4097}, std::size_t{8191}, std::size_t{8192}, std::size_t{8193}, + expected.size(), expected.size() + 1, maximum}) { + const auto count = std::min(expected.size(), limit); + require(reader.ReadPrefix(name, limit) == Bytes(expected.begin(), expected.begin() + count), + "Incorrect file prefix for limit " + std::to_string(limit)); + } +} + +void fileContents(const std::filesystem::path& directory) { + const auto path = directory / Io::NativePath("prefix-\xc3\xa9-\xe6\x97\xa5.bin"); + for (const std::size_t size : {std::size_t{0}, std::size_t{3}, std::size_t{4096}, std::size_t{4097}, + std::size_t{8192}, std::size_t{10003}}) { + Bytes expected(size); + for (std::size_t index = 0; index < size; ++index) + expected[index] = static_cast(index * 37 + 13); + std::ofstream output(path, std::ios::binary); + if (!expected.empty()) + output.write(reinterpret_cast(expected.data()), static_cast(expected.size())); + output.close(); + require(static_cast(output), "Cannot prepare the prefix-read fixture"); + prefixes(path, expected); + require(Io::FileReader().Read(Io::Utf8Path(path)) == expected, "Prefix reads changed the input file"); + } +} + +void invalidRequests(const std::filesystem::path& directory) { + const Io::FileReader reader; + const auto missing = Io::Utf8Path(directory / "missing.bin"); + const auto folder = Io::Utf8Path(directory); + for (const std::size_t size : {std::size_t{0}, std::size_t{1}, std::size_t{4097}}) { + requireError([&] { reader.ReadPrefix(missing, size); }, "Cannot open file: " + missing); + requireError([&] { reader.ReadPrefix(folder, size); }, "Cannot open file: " + folder); + } + const auto maximum = static_cast(std::numeric_limits::max()); + if (maximum < std::numeric_limits::max()) { + requireError([&] { reader.ReadPrefix(missing, maximum + 1); }, "File prefix is too large: " + missing); + } +} + +void zeroReportedFileSize() { +#ifdef __linux__ + const std::filesystem::path path = "/proc/self/cmdline"; + require(std::filesystem::is_regular_file(path) && std::filesystem::file_size(path) == 0, + "Expected a regular proc file with a zero reported size"); + std::ifstream input(path, std::ios::binary); + const Bytes expected{std::istreambuf_iterator(input), std::istreambuf_iterator()}; + require(!expected.empty(), "Cannot read the proc prefix fixture"); + prefixes(path, expected); +#endif +} + +} + +int main() { + const auto directory = std::filesystem::temp_directory_path() / + ("anyps5-file-reader-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + try { + std::filesystem::create_directory(directory); + fileContents(directory); + invalidRequests(directory); + zeroReportedFileSize(); + std::filesystem::remove_all(directory); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + std::error_code ignored; + std::filesystem::remove_all(directory, ignored); + return 1; + } + std::cout << "File prefix-read tests passed\n"; + return 0; +} 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 a69ef856ff..81b6e3d359 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -125,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 @@ -404,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/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index 8fe4507a89..82b31f6086 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace Relinker { @@ -79,6 +80,8 @@ struct GuestModuleInput { std::vector PatchedRanges; }; +std::vector> RemoveBackupCopies(std::vector& modules); + class GuestModuleBuilder { public: std::vector Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, bool windows, bool macos, bool toIntel, bool toRosetta, ISyscallScanner& syscallScanner, bool lazyBinding, const std::string& runPath, const std::set& excludedModules, const std::filesystem::path& root, const std::vector& runtimeDirectories, std::vector modules, Domain::IProgress& progress) const; 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/EhFrameReader.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp index 5f1b754c86..b141b5b4b8 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp @@ -140,6 +140,9 @@ private: const bool negative = (value >> 63) != 0; if (negative ? base < std::uint64_t{0} - value : value > std::numeric_limits::max() - base) throw RelinkerException("Strict filter: unwind pointer address overflowed", field); + } else if ((encoding & 0x0F) == 2 || (encoding & 0x0F) == 3) { + if (value > std::numeric_limits::max() - base) + throw RelinkerException("Strict filter: unwind pointer address overflowed", field); } value = base + value; } 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/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index 699f47df2a..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; } diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 7c2110ae58..e8f9496bf7 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 @@ -379,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) { @@ -395,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()) { @@ -471,4 +489,33 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path return artifacts; } + +std::vector> RemoveBackupCopies(std::vector& modules) { + std::map sonames; + const auto soname = [&](std::size_t index) -> const std::string& { + const auto [found, inserted] = sonames.try_emplace(index); + if (inserted) found->second = GuestImageReader().Read(modules[index].Path, modules[index].Bytes, true).Soname; + return found->second; + }; + std::vector> skipped; + std::vector copies(modules.size()); + for (std::size_t copy = 0; copy < modules.size(); ++copy) { + const auto& path = modules[copy].Path; + const auto name = Io::Utf8Path(path.filename()); + for (std::size_t original = 0; original < modules.size(); ++original) { + const auto& kept = modules[original].Path; + const auto prefix = Io::Utf8Path(kept.filename()); + if (kept.parent_path() != path.parent_path() || prefix.size() >= name.size() || !name.starts_with(prefix)) continue; + if (soname(copy).empty() || soname(copy) != soname(original)) continue; + copies[copy] = true; + skipped.emplace_back(path, kept); + break; + } + } + std::vector remaining; + for (std::size_t index = 0; index < modules.size(); ++index) + if (!copies[index]) remaining.push_back(std::move(modules[index])); + modules = std::move(remaining); + return skipped; +} } 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..628ec27254 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -52,6 +52,9 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti if (options.MacosFixedBase && options.Target != TargetSystem::MacOs) throw Domain::RelinkerException("A fixed macOS base requires the macOS target"); const bool windows = options.Target == TargetSystem::Windows; const bool macos = options.Target == TargetSystem::MacOs; + for (const auto& [copy, kept] : RemoveBackupCopies(input.Modules)) + inputProgress.Message("Skipping backup copy: " + copy.lexically_relative(input.ModuleRoot).generic_string() + " (same SONAME as " + + kept.lexically_relative(input.ModuleRoot).generic_string() + ")\n"); inputProgress.Report({Domain::ProgressStage::Executable, Io::Utf8Path(input.InputPath), 0, 1}); auto sourceBytes = std::move(input.Executable); std::vector appliedPatches; @@ -125,6 +128,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti std::copy(moved.Bytes.begin(), moved.Bytes.end(), sourceBytes.begin() + static_cast(moved.Offset)); } { std::ostringstream message; message << "Red-zone framing: " << report.Functions << " functions, " << report.RedZoneFunctions << " leaf functions keep values below RSP, " << report.Framed << " framed (" << report.FramedWithoutFramePointer << " without a frame pointer, " << report.MovedOperands << " rsp operands moved, " << report.StubbedOperands << " in " << report.OperandStubs << " operand stubs; " << report.Exits << " exits)\n"; inputProgress.Message(message.str()); } + if (report.WidenedPrologues != 0 || report.WidenedExits != 0) { std::ostringstream message; message << "Red-zone framing: " << report.WidenedPrologues << " prologues widened over the instructions after them and " << report.WidenedExits << " exits over the instructions before them, as they were shorter than a jump (" << report.WidenedOperands << " rsp operands moved into their stubs)\n"; inputProgress.Message(message.str()); } for (const auto& [reason, count] : report.Skipped) { std::ostringstream message; message << "Red-zone framing: skipped " << count << ": " << reason << '\n'; inputProgress.Message(message.str()); } trampolines.insert(trampolines.end(), std::make_move_iterator(framed.begin()), std::make_move_iterator(framed.end())); } @@ -155,7 +159,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 d61c99178a..b63ed31554 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -22,6 +22,8 @@ namespace { constexpr std::uint16_t kEtNone = 0; constexpr std::uint16_t kEtRel = 1; constexpr std::uint16_t kEtCore = 4; +constexpr std::uint16_t kEtSceDynExec = 0xfe10; +constexpr std::uint16_t kEtSceDynamic = 0xfe18; } @@ -64,6 +66,11 @@ RelinkResult RelinkerPipeline::Relink() { throw RelinkerException("ET_NONE inputs do not specify an ELF object type"); if (inputType == kEtCore) throw RelinkerException("ET_CORE core dumps are not executable images"); + if (inputType != ET_EXEC && inputType != ET_DYN && inputType != kEtSceDynExec && inputType != kEtSceDynamic) { + std::ostringstream message; + message << "Unsupported ELF object type 0x" << std::hex << inputType << ": expected ET_EXEC, ET_DYN, 0xfe10 or 0xfe18"; + throw RelinkerException(message.str(), 0x10); + } auto programHeaders = elfReader->ReadProgramHeaders(); @@ -103,10 +110,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) @@ -321,8 +329,9 @@ RelinkResult RelinkerPipeline::Relink() { return type == R_X86_64_DTPMOD64 || type == R_X86_64_DTPOFF64 || type == R_X86_64_TPOFF64; }; - const bool hasNonEmptyTlsSegment = std::any_of(programHeaders.begin(), programHeaders.end(), - [](const ProgramHeader& ph) { return ph.Type == PT_TLS && ph.MemorySize != 0; }); + 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); @@ -332,8 +341,14 @@ RelinkResult RelinkerPipeline::Relink() { return std::nullopt; if ((raw[symOff + 4] & 15) != STT_TLS) throw RelinkerException("TLS relocation names a defined symbol that is not STT_TLS", pos); + if (section >= SHN_LORESERVE) + throw RelinkerException("Unsupported executable TLS symbol section", pos); 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) { @@ -383,8 +398,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}); } }; @@ -393,17 +409,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; 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/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp index 4b27916736..48317b9b9a 100644 --- a/core/relinker/relinker/tests/ConversionTests.cpp +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -25,7 +25,7 @@ void reject(TAction action) { throw std::runtime_error("Invalid conversion was accepted"); } -std::vector fixture(const std::string& needed = {}) { +std::vector fixture(const std::string& needed = {}, const std::string& soname = {}) { std::vector bytes(0x3000); const std::array magic{0x7f, 'E', 'L', 'F', 2, 1, 1}; std::copy(magic.begin(), magic.end(), bytes.begin()); @@ -53,11 +53,16 @@ std::vector fixture(const std::string& needed = {}) { header(1, 1, 6, 0x2000, 0x1000); for (std::size_t index = 3; index < 8; ++index) header(index, 0x6fffff01, 0, 0, 0); bytes[0x1000] = 0xc3; - std::vector> tags{{5, 0x2600}, {10, needed.empty() ? 1 : needed.size() + 2}, {6, 0x2620}, {11, 24}, {7, 0x2700}, {8, 0}, {9, 24}, {4, 0x2680}}; + const std::size_t sonameOffset = needed.empty() ? 1 : needed.size() + 2; + std::vector> tags{{5, 0x2600}, {10, soname.empty() ? sonameOffset : sonameOffset + soname.size() + 1}, {6, 0x2620}, {11, 24}, {7, 0x2700}, {8, 0}, {9, 24}, {4, 0x2680}}; if (!needed.empty()) { std::copy(needed.begin(), needed.end(), bytes.begin() + 0x2601); tags.push_back({1, 1}); } + if (!soname.empty()) { + std::copy(soname.begin(), soname.end(), bytes.begin() + 0x2600 + static_cast(sonameOffset)); + tags.push_back({14, sonameOffset}); + } tags.push_back({0, 0}); header(2, 2, 6, 0x2000, tags.size() * 16); for (std::size_t index = 0; index < tags.size(); ++index) { @@ -127,6 +132,27 @@ void convert() { require(progress.Events.back().Completed != 1 || progress.Events.back().Stage != Domain::ProgressStage::Executable, "Failed conversion reported completion"); } +void backupCopies() { + for (const auto target : {Relinker::TargetSystem::Windows, Relinker::TargetSystem::Linux}) { + TestProgress progress; + Relinker::ConversionOptions options; + options.Target = target; + auto source = input(); + for (const auto* name : {"libSceFace.prx", "libSceFace.prx.esbak", "libSceFace.prxbak"}) + source.Modules.push_back({std::filesystem::path("virtual/title/sce_module") / name, fixture({}, "libSceFace.prx")}); + const auto output = Relinker::Conversion(progress).Convert(std::move(source), options); + require(output.Modules.size() == 1 && output.Modules.front().Path.generic_string() == "virtual/output/app0/sce_module/libSceFace.prx.guest.prx", "A backup copy was converted"); + for (const auto* name : {"libSceFace.prx.esbak", "libSceFace.prxbak"}) + require(progress.Messages.find(std::string("Skipping backup copy: sce_module/") + name + " (same SONAME as sce_module/libSceFace.prx)\n") != std::string::npos, "A skipped backup copy was not reported"); + } + TestProgress progress; + auto source = input(); + source.Modules.push_back({"virtual/title/sce_module/libSceFace.prx", fixture({}, "libSceFace.prx")}); + source.Modules.push_back({"virtual/title/sce_module/libSceFace.prx.esbak", fixture({}, "libSceFaceOld.prx")}); + const auto output = Relinker::Conversion(progress).Convert(std::move(source), {}); + require(output.Modules.size() == 2 && progress.Messages.find("Skipping backup copy") == std::string::npos, "A module with another SONAME was skipped as a backup copy"); +} + void runtimeLifecyclePolicy() { TestProgress progress; const Relinker::Conversion converter(progress); @@ -377,6 +403,7 @@ void relocationValidation() { int main() { try { convert(); + backupCopies(); runtimeLifecyclePolicy(); fixedBase(); scanning(); 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/UnwindRecordBoundsTests.cpp b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp index 31f110f1a8..7012706870 100644 --- a/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp +++ b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp @@ -34,10 +34,22 @@ int main() { Domain::ProgramHeader funcSeg{}; funcSeg.Type = 1; funcSeg.MappedAddress = 0x800; funcSeg.FileSize = 0x20; const auto below = Relinker::UnusedNidFilter::ReadExceptionFunctions(belowBytes, {funcSeg, load, eh}, {}, {}); Require(below.size() == 1 && below[0].Begin == 0x800 && below[0].End == 0x810); + auto belowDatarelUdata8 = Fixture(); + belowDatarelUdata8[3] = 0x34; + Io::WriteU64(belowDatarelUdata8, 16, 0x800 - 0x1000); + Io::WriteU64(belowDatarelUdata8, 24, 0x1060 - 0x1000); + Io::WriteU64(belowDatarelUdata8, 104, 0x800); + Require(Relinker::UnusedNidFilter::ReadExceptionFunctions(belowDatarelUdata8, {funcSeg, load, eh}, {}, {}).size() == 1); + auto belowPcrelUdata8 = Fixture(); + belowPcrelUdata8[3] = 0x14; + Io::WriteU64(belowPcrelUdata8, 16, 0x800 - 0x1010); + Io::WriteU64(belowPcrelUdata8, 24, 0x1060 - 0x1018); + Io::WriteU64(belowPcrelUdata8, 104, 0x800); + Require(Relinker::UnusedNidFilter::ReadExceptionFunctions(belowPcrelUdata8, {funcSeg, load, eh}, {}, {}).size() == 1); auto empty = Fixture(); Io::WriteU64(empty, 112, 0); Require(Relinker::UnusedNidFilter::ReadExceptionFunctions(empty, {load, eh}, {}, {}).empty()); - for (int kind = 0; kind < 10; ++kind) { + for (int kind = 0; kind < 11; ++kind) { auto bytes = Fixture(); auto segment = load; switch (kind) { case 0: Io::WriteU32(bytes, 64, 0x1000); break; @@ -56,6 +68,16 @@ int main() { break; } case 9: bytes[3] = 0x1C; Io::WriteU64(bytes, 16, 0x8000000000000000ULL); break; + case 10: { + segment.MappedAddress = 0xFFFFFFFFFFFF0000ULL; + eh.MappedAddress = 0xFFFFFFFFFFFF0000ULL; + Io::WriteU64(bytes, 4, 0xFFFFFFFFFFFF0040ULL); + bytes[3] = 0x13; + Io::WriteU32(bytes, 16, 0x107F0); + Io::WriteU32(bytes, 20, 0x4C); + Io::WriteU64(bytes, 104, 0x800); + break; + } } bool rejected = false; std::vector headers = {segment, eh}; @@ -63,10 +85,14 @@ int main() { Domain::ProgramHeader high{}; high.Type = 1; high.MappedAddress = 0xFFFFFFFFFFFFF000ULL; high.FileSize = 0x100; headers.push_back(high); } + if (kind == 10) { + Domain::ProgramHeader funcSeg{}; funcSeg.Type = 1; funcSeg.MappedAddress = 0x800; funcSeg.FileSize = 0x20; + headers = {segment, eh, funcSeg}; + } try { Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); } catch (const std::exception&) { rejected = true; } Require(rejected); } - std::cout << "14 unwind record cases passed\n"; + std::cout << "17 unwind record cases passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } } 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_segments.py b/core/relinker/relinker/tests/test_executable_segments.py index 34e3d1ae4b..862ff1f962 100644 --- a/core/relinker/relinker/tests/test_executable_segments.py +++ b/core/relinker/relinker/tests/test_executable_segments.py @@ -118,15 +118,19 @@ def main(): result, output = run_case(relinker, work, "strict", ["unused-filter=2"], b"\x90\xc3") assert result.returncode == 2 and "multiple executable segments" in result.stderr and not output.exists(), result - for name, memory_size in (("overlap-past-end", 0x10000000000000), ("overlap-next", 0x1300)): + image_targets = ([], ["--lazy-binding"], ["--linux-fixed-base", "0x400000"], ["--windows"], ["--macos"]) + for name, memory_size, targets in (("overlap-past-end", 0x10000000000000, image_targets[3:]), + ("overlap-next", 0x1300, image_targets)): image = fixture(b"\x90\xc3") 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_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_jmprel_table_bounds.py b/core/relinker/relinker/tests/test_jmprel_table_bounds.py index ed7bb8f41e..fa03c42e42 100644 --- a/core/relinker/relinker/tests/test_jmprel_table_bounds.py +++ b/core/relinker/relinker/tests/test_jmprel_table_bounds.py @@ -24,8 +24,17 @@ DT_OS_PLTRELSZ = 0x6100002D DT_OS_SYMTABSZ = 0x6100003F +TARGET_OPTIONS = { + "windows": ["--windows"], + "macos": ["--macos"], + "lazy": ["--lazy-binding"], + "fixed": ["--linux-fixed-base", "0x10000000"], +} + + def fixture(jmprel=0x700, pltrelsz=24, os_tags=True, target=0x300, symtab=0x620, - symbol_bytes=48, symbol_index=1, relocation_type=None, data_relocations=False, linux=False): + symbol_bytes=48, symbol_index=1, relocation_type=None, data_relocations=False, linux=False, + addend=0): image = bytearray(0x1000) if relocation_type is None: relocation_type = 6 if data_relocations else 7 @@ -62,7 +71,7 @@ def fixture(jmprel=0x700, pltrelsz=24, os_tags=True, target=0x300, symtab=0x620, 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_patch_advisor.py b/core/relinker/relinker/tests/test_patch_advisor.py index bed41997bd..e18a165784 100644 --- a/core/relinker/relinker/tests/test_patch_advisor.py +++ b/core/relinker/relinker/tests/test_patch_advisor.py @@ -15,7 +15,7 @@ def main(): image = bytes(main_fixture()) address, expected = patch_site(image) with tempfile.TemporaryDirectory(prefix="anyps5-patch-advisor-") as directory: - root = Path(directory) + root = Path(directory).resolve() source = root / "eboot.bin" source.write_bytes(image) catalog = root / "catalog" @@ -25,28 +25,43 @@ def main(): metadata = root / "param.json" metadata.write_text(json.dumps({"titleId": "TEST00001", "contentVersion": "01.000.000"})) - def advise(digest, patches=("patch.toml",)): + def advise(digest, patches=("patch.toml",), *options): (catalog / "index.json").write_text(json.dumps({"format": 1, "entries": [{ "titleId": "TEST00001", "contentVersion": "01.000.000", "patch": name, "targets": {"eboot.bin": digest} } for name in patches]})) return subprocess.run([sys.executable, str(advisor), "--catalog", str(catalog), "--param-json", str(metadata), "--input", str(source), - "--relinker", f"./{relinker.name}", "--skip-sce-module"], + "--relinker", f"./{relinker.name}", "--skip-sce-module", *options], cwd=relinker.parent, capture_output=True, text=True, timeout=30) - result = advise(hashlib.sha256(image).hexdigest()) + digest = hashlib.sha256(image).hexdigest() + result = advise(digest) assert result.returncode == 0 and f"Recommended: {patch}" in result.stdout, (result.stdout, result.stderr) + second = catalog / "second patch.toml" + second.write_text(patch_text(address, expected, "flip_bit")) + result = advise(digest, ("patch.toml", second.name)) + assert result.returncode == 1 and "multiple verified catalog entries match; use --list-verified to see them" in result.stdout, (result.stdout, result.stderr) + result = advise(digest, ("patch.toml", second.name), "--list-verified") + assert result.returncode == 0 and "Verified catalog entries: 2" in result.stdout, (result.stdout, result.stderr) + assert "Choose one patch file; listed entries are not combined." in result.stdout, (result.stdout, result.stderr) + assert f"Catalog entry 1: {patch}" in result.stdout and f"Catalog entry 2: {second}" in result.stdout, (result.stdout, result.stderr) + assert "Recommended:" not in result.stdout, (result.stdout, result.stderr) + assert source.read_bytes() == image broken = catalog / "broken.toml" broken.write_text(patch_text(address, expected, "broken").replace('description = "flip one bit"', r'description = "flip \"one\" bit"')) - result = advise(hashlib.sha256(image).hexdigest(), ("broken.toml", "patch.toml")) + result = advise(digest, ("broken.toml", "patch.toml")) assert result.returncode == 0 and "relinker error (exit 2)" in result.stdout and f"Recommended: {patch}" in result.stdout, (result.stdout, result.stderr) - result = advise(hashlib.sha256(image).hexdigest(), ("broken.toml",)) + result = advise(digest, ("broken.toml", "patch.toml"), "--list-verified") + assert result.returncode == 0 and "relinker error (exit 2)" in result.stdout and "Verified catalog entries: 1" in result.stdout, (result.stdout, result.stderr) + result = advise(digest, ("broken.toml",), "--list-verified") assert result.returncode == 2 and "relinker error (exit 2)" in result.stdout, (result.stdout, result.stderr) patch.write_text(patch_text(address, expected ^ 1, "wrong_byte")) - result = advise(hashlib.sha256(image).hexdigest()) + result = advise(digest) assert result.returncode == 1 and "rejected by relinker" in result.stdout, (result.stdout, result.stderr) + result = advise(digest, ("patch.toml",), "--list-verified") + assert result.returncode == 1 and "rejected by relinker" in result.stdout and "Catalog entry 1:" not in result.stdout, (result.stdout, result.stderr) patch.write_text(patch_text(address, expected, "flip_bit")) result = advise(hashlib.sha256(b"other build").hexdigest()) assert result.returncode == 1 and "SHA-256 mismatch" in result.stdout, (result.stdout, result.stderr) diff --git a/core/relinker/relinker/tests/test_sce_code_extent.py b/core/relinker/relinker/tests/test_sce_code_extent.py index 1f58fb585e..e7dafe53c9 100644 --- a/core/relinker/relinker/tests/test_sce_code_extent.py +++ b/core/relinker/relinker/tests/test_sce_code_extent.py @@ -26,17 +26,22 @@ UNSUPPORTED_TLS = bytes.fromhex("64 2d 65 6c 66 2e c3") INVALID = bytes.fromhex("41 41 41 41 41 41 66 66 66 66 2e 0f 1f 84 00 00 00 00 00") TLS_LOAD = bytes.fromhex("64 48 8b 04 25 00 00 00 00 c3") FS_SUBTRACT = bytes.fromhex("64 2d 65 6c 66 2e") +LATER_FUNCTION = 0x300 def fixture(sce_block=True, plt_push_offset=8, gap=0, interpreter_pointer=False, data_pointer=False, gap_entry=False, plt_at=None, early_function=False, function_tls=False, pointer_target=None, gap_bytes=None, functions=None, - tls=False): - plt = FUNCTION + FUNCTION_SIZE + gap if plt_at is None else plt_at + tls=False, gap_embedded_data=False, gap_data_then_syscall=False, gap_callback_syscall_undecodable=False, + gap_decoded_padding_syscall=False, gap_stub_then_data=False): + has_gap_embedded = gap_embedded_data or gap_data_then_syscall or gap_callback_syscall_undecodable or gap_decoded_padding_syscall or gap_stub_then_data + plt = (LATER_FUNCTION + FUNCTION_SIZE + gap if has_gap_embedded else FUNCTION + FUNCTION_SIZE + gap) if plt_at is None else plt_at entry = FUNCTION + FUNCTION_SIZE if gap_entry else FUNCTION data = plt + 0x20 if plt > 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 3a10407df0..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 033167b746..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); @@ -1595,7 +1724,7 @@ void emitComponent(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); } } @@ -2162,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; @@ -2225,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); } } @@ -2246,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; @@ -2489,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; @@ -2519,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) { @@ -2540,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; } @@ -2554,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; @@ -2571,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/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index f9a4d4e0b9..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) { diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 89ee86f9d6..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) + ")"); } @@ -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/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/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 result; std::uint64_t used = 0; std::shared_ptr patch; + std::uint64_t bytes = 0; }; struct EmissionFailure { @@ -399,7 +400,6 @@ struct SourceEntry : std::enable_shared_from_this { std::vector emissionFailures; std::unique_ptr memo; std::unique_ptr maskedMemo; - std::uint64_t memoClock = 0; }; namespace { @@ -1455,8 +1455,9 @@ ResultMemoEntry* memoSet(const std::unique_ptr& table, std::uin } struct ResultMemoCounters { - std::atomic hits{0}, patched{0}, misses{0}, evictions{0}, populateNanoseconds{0}, patchNanoseconds{0}, mismatches{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) { @@ -1503,6 +1504,123 @@ ResultMemoCounters& resultMemoCounters() { 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; @@ -1526,11 +1644,12 @@ void reportResultMemo() { 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); 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\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)); + 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 @@ -1644,7 +1763,7 @@ std::shared_ptr materializePreparedMemoized(SourceEntry& for (std::size_t way = 0; way < ResultMemoWays; ++way) { auto& entry = set[way]; if (entry.result == nullptr || entry.variantId != variantId || entry.hash != hash) continue; - entry.used = ++source.memoClock; + entry.used = nextMemoUse(); counters.hits.fetch_add(1, std::memory_order_relaxed); if (memoHit != nullptr) *memoHit = true; reportResultMemo(); @@ -1663,7 +1782,7 @@ std::shared_ptr materializePreparedMemoized(SourceEntry& 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 = ++source.memoClock; + entry.used = nextMemoUse(); donor = entry.result; donorPatch = entry.patch; break; @@ -1687,20 +1806,35 @@ std::shared_ptr materializePreparedMemoized(SourceEntry& } 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 (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 = ++source.memoClock; + entry.used = nextMemoUse(); return entry.result; } if (entry.used < victim->used) victim = &entry; } - if (count && victim->result != nullptr) counters.evictions.fetch_add(1, std::memory_order_relaxed); - *victim = {variantId, key, shared, ++source.memoClock, std::move(recipe)}; + 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; }; { @@ -1708,6 +1842,7 @@ std::shared_ptr materializePreparedMemoized(SourceEntry& 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; } @@ -1940,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 212e6b2f8d..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 { @@ -826,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/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/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 5c75b23f82..fda431fc05 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -171,6 +171,7 @@ struct SpirvEmitterState { std::uint32_t reciprocalCorrectionFunction = 0; std::array reciprocalTables {}; std::map, std::uint32_t> interpolateFunctions; + std::map, std::uint32_t> fastInterpolateFunctions; bool integerInterpolation = false; std::array, 2> bdaDwordReadFunctions {}; std::array, 2> bdaSpanReadFunctions {}; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp index 2cf74380c7..7ce42d522b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp @@ -946,9 +946,80 @@ std::uint32_t EmitFPDot2F32F16(SpirvEmitterState& state, std::uint32_t arg0, std namespace { -std::uint32_t InterpolateFunction(SpirvEmitterState& state, bool halfResult, std::uint32_t mode) { +std::uint32_t InterpolateFast(SpirvEmitterState& state, std::uint32_t delta, std::uint32_t coordinate, std::uint32_t base, bool halfResult, std::uint32_t mode, std::uint32_t& fast) { + const auto u32 = TypeU32(state); + const auto f32 = TypeF32(state); + const auto boolean = TypeBool(state); + const bool flush32 = (mode & InterpolationFlush32) != 0u; + const bool flush16 = (mode & InterpolationFlush16) != 0u; + delta = FlushDenormal32(state, delta, flush16, 0x38800000u); + coordinate = FlushDenormal32(state, coordinate, flush32, 0x00800000u); + base = halfResult ? FlushDenormal32(state, base, flush32, 0x00800000u) : FlushDenormal32(state, base, flush16, 0x38800000u); + const auto op = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t value) { return Binary(state, opcode, u32, lhs, ConstantU32(state, value)); }; + const auto test = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t value) { return Binary(state, opcode, boolean, lhs, ConstantU32(state, value)); }; + const auto all = [&](std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, spv::OpLogicalAnd, boolean, lhs, rhs); }; + const auto any = [&](std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, spv::OpLogicalOr, boolean, lhs, rhs); }; + const auto bits = [&](std::uint32_t value) { return Unary(state, spv::OpBitcast, u32, value); }; + const auto zero = [&](std::uint32_t value) { return test(spv::OpIEqual, op(spv::OpBitwiseAnd, bits(value), 0x7fffffffu), 0u); }; + const auto exponent = [&](std::uint32_t value) { return op(spv::OpBitwiseAnd, op(spv::OpShiftRightLogical, bits(value), 23u), 0xffu); }; + const auto within = [&](std::uint32_t field, std::uint32_t lowest, std::uint32_t highest) { return test(spv::OpULessThanEqual, op(spv::OpISub, field, lowest), highest - lowest); }; + const auto zeroDelta = zero(delta); + const auto zeroCoordinate = zero(coordinate); + const auto exponentDelta = exponent(delta); + const auto exponentCoordinate = exponent(coordinate); + fast = all(any(zeroDelta, within(exponentDelta, 24u, 241u)), any(zeroCoordinate, within(exponentCoordinate, 24u, 241u))); + const auto zeroBase = zero(base); + fast = all(fast, any(zeroBase, within(exponent(base), 24u, 251u))); + fast = all(fast, any(any(zeroDelta, zeroCoordinate), within(Binary(state, spv::OpIAdd, u32, exponentDelta, exponentCoordinate), 174u, 377u))); + const auto split = [&](std::uint32_t value) { + const auto spread = ExactF32(state, spv::OpFMul, value, ConstantF32(state, 0x45800800u)); + const auto high = ExactF32(state, spv::OpFSub, spread, ExactF32(state, spv::OpFSub, spread, value)); + return std::array{high, ExactF32(state, spv::OpFSub, value, high)}; + }; + const auto splitDelta = split(delta); + const auto splitCoordinate = split(coordinate); + const auto product = ExactF32(state, spv::OpFMul, delta, coordinate); + auto productError = ExactF32(state, spv::OpFSub, ExactF32(state, spv::OpFMul, splitDelta[0], splitCoordinate[0]), product); + productError = ExactF32(state, spv::OpFAdd, productError, ExactF32(state, spv::OpFMul, splitDelta[0], splitCoordinate[1])); + productError = ExactF32(state, spv::OpFAdd, productError, ExactF32(state, spv::OpFMul, splitDelta[1], splitCoordinate[0])); + productError = ExactF32(state, spv::OpFAdd, productError, ExactF32(state, spv::OpFMul, splitDelta[1], splitCoordinate[1])); + const auto upper = TwoSumF32(state, base, product); + const auto lower = TwoSumF32(state, upper[1], productError); + const auto nearestBits = bits(lower[0]); + const auto errorBits = bits(lower[1]); + const auto inexact = test(spv::OpINotEqual, op(spv::OpBitwiseAnd, errorBits, 0x7fffffffu), 0u); + const auto even = test(spv::OpIEqual, op(spv::OpBitwiseAnd, nearestBits, 1u), 0u); + const auto sameSign = test(spv::OpIEqual, op(spv::OpBitwiseAnd, Binary(state, spv::OpBitwiseXor, u32, nearestBits, errorBits), 0x80000000u), 0u); + const auto odd = Binary(state, spv::OpIAdd, u32, nearestBits, Select(state, u32, sameSign, ConstantU32(state, 1u), ConstantU32(state, 0xffffffffu))); + const auto roundedToOdd = Unary(state, spv::OpBitcast, f32, Select(state, u32, all(inexact, even), odd, nearestBits)); + const auto total = TwoSumF32(state, upper[0], roundedToOdd); + const auto signs = Binary(state, spv::OpBitwiseAnd, u32, Binary(state, spv::OpBitwiseXor, u32, bits(delta), bits(coordinate)), bits(base)); + const auto zeroSign = Select(state, u32, all(any(zeroDelta, zeroCoordinate), zeroBase), op(spv::OpBitwiseAnd, signs, 0x80000000u), ConstantU32(state, 0u)); + const auto roundedBits = bits(total[0]); + const auto resultBits = Select(state, u32, test(spv::OpIEqual, op(spv::OpBitwiseAnd, roundedBits, 0x7fffffffu), 0u), zeroSign, roundedBits); + if (!halfResult) return Unary(state, spv::OpBitcast, f32, resultBits); + const auto magnitude = op(spv::OpBitwiseAnd, resultBits, 0x7fffffffu); + const auto magnitudeValue = Unary(state, spv::OpBitcast, f32, magnitude); + const auto shifted = ExactF32(state, spv::OpFSub, ExactF32(state, spv::OpFAdd, magnitudeValue, ConstantF32(state, 0x3f400000u)), ConstantF32(state, 0x3f400000u)); + const auto rest = bits(ExactF32(state, spv::OpFSub, magnitudeValue, shifted)); + const auto subnormal = test(spv::OpULessThan, magnitude, 0x38800000u); + const auto normalMidpoint = test(spv::OpIEqual, op(spv::OpBitwiseAnd, resultBits, 0x1fffu), 0x1000u); + const auto subnormalMidpoint = test(spv::OpIEqual, op(spv::OpBitwiseAnd, rest, 0x7fffffffu), 0x33000000u); + const auto midpoint = Select(state, boolean, subnormal, subnormalMidpoint, normalMidpoint); + const auto exact = all(Unary(state, spv::OpLogicalNot, boolean, inexact), test(spv::OpIEqual, op(spv::OpBitwiseAnd, bits(total[1]), 0x7fffffffu), 0u)); + fast = all(fast, all(test(spv::OpULessThan, magnitude, 0x477ff000u), any(Unary(state, spv::OpLogicalNot, boolean, midpoint), exact))); + const auto increment = Binary(state, spv::OpIAdd, u32, op(spv::OpBitwiseAnd, op(spv::OpShiftRightLogical, resultBits, 13u), 1u), ConstantU32(state, 0x0fffu)); + const auto normal = op(spv::OpBitwiseAnd, Binary(state, spv::OpIAdd, u32, resultBits, increment), 0xffffe000u); + auto small = bits(shifted); + if (flush16) small = Select(state, u32, test(spv::OpULessThan, small, 0x38800000u), ConstantU32(state, 0u), small); + small = Binary(state, spv::OpBitwiseOr, u32, op(spv::OpBitwiseAnd, resultBits, 0x80000000u), small); + return Unary(state, spv::OpBitcast, f32, Select(state, u32, subnormal, small, normal)); +} + +template +std::uint32_t InterpolateHelper(SpirvEmitterState& state, std::map, std::uint32_t>& functions, bool halfResult, std::uint32_t mode, const char* suffix, std::uint32_t control, TBody&& body) { const auto key = std::make_pair(halfResult, mode); - if (const auto found = state.interpolateFunctions.find(key); found != state.interpolateFunctions.end()) return found->second; + if (const auto found = functions.find(key); found != functions.end()) return found->second; const auto f32 = TypeF32(state); const auto id = state.module.AllocateId(); struct Restore { @@ -961,22 +1032,42 @@ std::uint32_t InterpolateFunction(SpirvEmitterState& state, bool halfResult, std }; state.module.BeginHelperFunction(); const Restore restore{state, state.currentLabel}; - state.module.AddName(id, std::string(halfResult ? "interpolate_f16_mode_" : "interpolate_f32_mode_") + std::to_string(mode)); - state.module.AddFunction(spv::OpFunction, f32, id, spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, f32, f32, f32, f32)); + state.module.AddName(id, std::string(halfResult ? "interpolate_f16_mode_" : "interpolate_f32_mode_") + std::to_string(mode) + suffix); + state.module.AddFunction(spv::OpFunction, f32, id, control, state.module.Type(spv::OpTypeFunction, f32, f32, f32, f32)); std::array parameters{}; for (auto& parameter : parameters) { parameter = state.module.AllocateId(); state.module.AddFunction(spv::OpFunctionParameter, f32, parameter); } EmitLabel(state, state.module.AllocateId()); - state.module.AddFunction(spv::OpReturnValue, (state.integerInterpolation ? InterpolateInteger : Interpolate)(state, parameters[0], parameters[1], parameters[2], halfResult, mode)); + state.module.AddFunction(spv::OpReturnValue, body(parameters)); state.module.AddFunction(spv::OpFunctionEnd); - state.interpolateFunctions.emplace(key, id); + functions.emplace(key, id); return id; } +std::uint32_t InterpolateFunction(SpirvEmitterState& state, bool halfResult, std::uint32_t mode) { + return InterpolateHelper(state, state.interpolateFunctions, halfResult, mode, "", spv::FunctionControlDontInlineMask, [&](const std::array& parameters) { + return (state.integerInterpolation ? InterpolateInteger : Interpolate)(state, parameters[0], parameters[1], parameters[2], halfResult, mode); + }); +} + +std::uint32_t FastInterpolateFunction(SpirvEmitterState& state, bool halfResult, std::uint32_t mode) { + const auto exact = InterpolateFunction(state, halfResult, mode); + return InterpolateHelper(state, state.fastInterpolateFunctions, halfResult, mode, "_fast", spv::FunctionControlMaskNone, [&](const std::array& parameters) { + std::uint32_t fast = 0; + const auto fastResult = InterpolateFast(state, parameters[0], parameters[1], parameters[2], halfResult, mode, fast); + return EmitValueOrDefaultIfCondition(state, Unary(state, spv::OpLogicalNot, TypeBool(state), fast), TypeF32(state), fastResult, [&] { + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, TypeF32(state), result, exact, parameters[0], parameters[1], parameters[2]); + return result; + }); + }); +} + std::uint32_t CallInterpolate(SpirvEmitterState& state, std::uint32_t delta, std::uint32_t coordinate, std::uint32_t base, bool halfResult, std::uint32_t mode) { - const auto function = InterpolateFunction(state, halfResult, mode); + const bool directed = halfResult && ((mode >> InterpolationRound16Shift) & 3u) != 0u; + const auto function = directed ? InterpolateFunction(state, halfResult, mode) : FastInterpolateFunction(state, halfResult, mode); const auto result = state.module.AllocateId(); state.module.AddFunction(spv::OpFunctionCall, TypeF32(state), result, function, delta, coordinate, base); return result; 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/docs/dev/BUILD.md b/docs/dev/BUILD.md index dfef62f9b1..56af412d8d 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -113,6 +113,7 @@ Project switches accept `ON` or `OFF`: | `-DANYPS5_BUILD_WASM=ON` | `OFF` | Build only the browser relinker (`relinker_web` target). Requires `emcmake` from Emscripten and `BUILD_TESTING=OFF`. | | `-DANYPS5_ENABLE_SPIRV_TOOLS=ON` | `OFF` | Enable SPIR-V validation and optimization. | | `-DANYPS5_ENABLE_ASAN=ON` | `OFF` | Build with AddressSanitizer on Linux and move the guest arena out of ASan's shadow gap. | +| `-DANYPS5_ENABLE_TSAN=ON` | `OFF` | Build with ThreadSanitizer on Linux and link executables dynamically. Cannot be combined with `-DANYPS5_ENABLE_ASAN=ON`. | | `-DANYPS5_USE_SYSTEM_DEPS=ON` | `OFF` | Resolve SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools from the system instead of the submodules. | | `-DAPS5_ENABLE_TIMING_LOG=ON` | `OFF` | Compile frame timing logging. | | `-DAPS5_ENABLE_TRACY=ON` | `OFF` | Build the [Tracy](#tracy) profiler client into the agc driver. | @@ -134,6 +135,10 @@ SDL and FreeType settings forced by the root `CMakeLists.txt` cannot be overridd With `ANYPS5_USE_SYSTEM_DEPS=ON` those libraries come from the system (on Debian and Ubuntu: `libsdl2-dev`, `libfreetype-dev`, `libvulkan-dev`, `spirv-headers`, `glslang-dev`, `glslang-tools` and `spirv-tools`). A missing package, Vulkan-Headers older than 1.4.310 (the agc driver uses `VK_KHR_maintenance8`), or an SDL2 without the HIDAPI joystick driver (sdl2-compat over SDL3, or a build with HIDAPI disabled, which breaks the pad and disk-audio tests), fails the configure with an error and never falls back to the submodule. The forced SDL and FreeType settings from the root `CMakeLists.txt` do not apply with the flag ON: the system SDL2 comes with whatever its distribution enabled. The prx libraries are shared objects, so SDL2 is linked from the system as a shared library; the default build keeps its own static SDL2. `ffmpeg-core`, `libjpeg-turbo`, `stb`, `VulkanMemoryAllocator`, `LibAtrac9`, `ogg` and `vorbis` always stay bundled and still need `git submodule update --init`. +## ThreadSanitizer + +A ThreadSanitizer build runs its tests with `TSAN_OPTIONS=suppressions=$PWD/tools/tsan-suppressions.txt`, which silences reports from Mesa's lavapipe, the LLVM it compiles shaders with, and PulseAudio: those system libraries are not instrumented, so ThreadSanitizer cannot see their locking. Guest memory written by the emulated GPU while a guest thread polls it, such as labels and fences, is shared without C++ atomics on purpose, as on the console, so ThreadSanitizer also reports those accesses. + ## Quick syntax checks After configuring a build directory, use `tools/check_compile.py` to check the given source files. The tool reads their compile commands from `compile_commands.json` or Ninja, preserving compilation options while dropping object and dependency output options. It handles `-MF`, `-MT`, `-MQ` and `-MJ` with separate or attached values so that unused output options do not cause false failures under `-Werror`. @@ -144,6 +149,28 @@ python3 tools/check_compile.py --build build-relinker core/relinker/main.cpp Real compile errors still fail. Syntax checks do not replace a full build and test run. +## Repeated Vulkan validation + +`tools/vulkan_validation_gate.py` builds two pinned clean source worktrees into a new output directory and alternates the same focused CTests between them. Execution currently supports POSIX hosts only. Linux hardware runs establish GPU behaviour; macOS synthetic tool tests do not establish native Linux/Windows or GPU correctness. Windows is refused before creating output. + +```sh +python3 tools/vulkan_validation_gate.py \ + --baseline /path/to/main FULL_MAIN_COMMIT \ + --candidate /path/to/change FULL_CHANGE_COMMIT \ + --output /path/to/new-validation-run \ + --target agc_driver_recorder_tests --target libs \ + --tests '^agc_recorder_completion_labels$' --gpu 'expected device name' \ + --repeat 20 --runtime core/libs/libs +``` + +Both worktrees must have the expected full commit ID and no tracked or untracked source changes. Output must be outside both worktrees and must not already exist. Each build uses Ninja, Release and `BUILD_TESTING=ON`; shared cache options can be supplied as `--cmake-arg=-DNAME=VALUE`. Test targets are explicit; the tool does not infer them. Configure and build logs, selected CTest commands, compiler/build configuration, commit IDs and hashes of selected command files and explicit build-relative `--runtime` files/directories are retained. Other dynamically loaded libraries and ignored source files are not inventoried. A clean commit alone does not identify an arbitrary existing binary, so existing build directories are not accepted. + +Each iteration runs serially with `ANYPS5_REQUIRE_VULKAN=1`, `ANYPS5_NO_SHADER_CACHE=1`, `VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation` and the requested `ANYPS5_GPU`. The tool rejects declared test properties that override those values or the recorded `VK_ICD_FILENAMES`, `VK_DRIVER_FILES` and `VK_LAYER_PATH` values. The GPU name is a selection request, not an independent hardware measurement. Every iteration retains verbose output and JUnit, with CTest's passed/failed output-size limits disabled so diagnostics near either end of a long output are retained. Missing, disabled or skipped selected tests, test failures and the exact missing-layer phrase fail the gate, even if CTest returns zero. + +The gate compares VUID and synchronization-hazard IDs per test across all baseline and candidate iterations. A new candidate ID fails; occurrence counts are reported without treating a count increase as a new violation. Existing baseline diagnostics remain visible and do not establish a clean validation run. Parsing covers standard `Validation Error/Warning:` and `VUID/SYNC-HAZARD (ERROR/WARNING / ...)` messages in JUnit test-body output, not arbitrary application formats or verbose CTest configuration text. Retain the logs for manual review of other formats. Source identity and selected artifact hashes are checked again after testing. + +`report.json` records the comparison and execution order. Exit 0 means the comparison completed with no detected regression, 1 means a test/coverage/layer or new diagnostic failure, 2 means setup, attribution or incomplete evidence, and 130 means interrupted. `--test-timeout` supplies CTest's default timeout; explicit test timeout properties still apply. `--timeout` bounds each configure/build/CTest command. The active POSIX process group is killed on completion, timeout or interruption. Repetitions report observed outcomes; they do not prove absence of intermittent faults or statistical significance. + ## Write watch tests The macOS write watch can be built and tested separately, without submodules or the prx libraries: @@ -195,7 +222,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. diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 911223394b..19cc8dcfb4 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -42,6 +42,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `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 | @@ -51,6 +52,7 @@ 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 | @@ -73,7 +75,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| | `ANYPS5_GPU` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | restricts physical device selection to device names containing this case-insensitive substring | -| `ANYPS5_OVERLAY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp` | 0 | 1 shows the performance overlay; 0 or unset hides it, and any other value is refused | +| `ANYPS5_OVERLAY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp` | 0 | the performance overlay's page at start: 1 the overlay, 2 its detailed page; 0 or unset hides it, and any other value is refused | | `ANYPS5_OVERLAY_POS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp` | tl | the overlay's corner: tl, tr, bl or br | | `ANYPS5_PRESENT_MODE` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | fifo | selects the swapchain present mode: fifo, fifo_latest_ready, mailbox or immediate | | `ANYPS5_REQUIRE_DESCRIPTOR_BINDING_FLAGS` | presence | `core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp` | | fails the replay test instead of accepting a device that refuses descriptor binding flags | @@ -220,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) | @@ -302,11 +305,12 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_PROC_TABLE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | resolves the device's Vulkan entry points per call instead of filling the function table once | | `APS5_NO_QUEUED_DRAW_READ_ORDER` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp` | | stops queued draws and DMA copies from holding back write-backs over the guest memory they read at commit | | `APS5_NO_QUEUED_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | disables the queued-label table | +| `APS5_NO_RECENT_IMPORTS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | looks every host import up in the import map instead of first checking the last four found while the map is unchanged | | `APS5_NO_RECORDED_CLEAR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | waits for a storage image's fast clear in a batch of its own instead of recording it | | `APS5_NO_RECORDER_SYNC_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | gives the draws the recorder waits for their own batch instead of recording them with the wait | | `APS5_NO_RECORD_COPIED_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | waits for copied-write draws at once instead of listing their write-back for later | | `APS5_NO_RECT_INDIRECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp` | | sends an indirect rectangle-list draw down the CPU path instead of the GPU-side path | -| `APS5_NO_RESIDENT_LINEAR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | drops the residency of a linear color target whose address already has a host import, so it is copied in and out again | +| `APS5_NO_RESIDENT_LINEAR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | drops the residency of linear color targets, so a target whose address has a host import, or whose import a draw would make, is copied in and out again | | `APS5_NO_RESIDENT_PRESENT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | always presents through guest memory instead of the resident image | | `APS5_NO_RESIDENT_TARGETS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | copies every render target in and out again instead of keeping it resident | | `APS5_NO_RESOURCE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables the shared resource cache, so every dispatch builds its resources fresh | @@ -539,6 +543,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| +| `ANYPS5_FULLSCREEN` | presence | `core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp` | | opens the display window fullscreen at the desktop resolution when set to a non-empty value other than 0; F11 still switches back | | `ANYPS5_INPUT_CONFIG` | string | `core/libs/prx/libSceVideoOut/src/InputMapping.cpp` | | path of the input mapping file that binds host keys and mouse to guest pad actions | | `ANYPS5_REQUIRE_DISPLAY` | presence | `core/libs/prx/libSceVideoOut/tests/Flip.cpp` | | makes video-out tests fail instead of skipping when no display or Vulkan device is available | | `APS5_NO_PAD_INPUT` | presence | `core/libs/prx/libSceVideoOut/src/PadInput.cpp` | | ignores keyboard and mouse input so a measurement run is not advanced by a stray press | diff --git a/docs/dev/GAME_TESTING.md b/docs/dev/GAME_TESTING.md index 82afb1ccfc..de2c53b6f3 100644 --- a/docs/dev/GAME_TESTING.md +++ b/docs/dev/GAME_TESTING.md @@ -15,6 +15,9 @@ For user options see [USAGE](../user/USAGE.md); for internals see [ARCHITECTURE] - 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/REVERSE_ENGINEERING.md b/docs/dev/REVERSE_ENGINEERING.md index bcbcd1ef3a..ec217b1936 100644 --- a/docs/dev/REVERSE_ENGINEERING.md +++ b/docs/dev/REVERSE_ENGINEERING.md @@ -63,6 +63,12 @@ read that always reports no data, or a write that drops its data, is not UI. grep -nE 'call +0x1f2e480$' text.dis # callers of the JUMP_SLOT stub at 0x1f2e480 ``` + For a relinked module (`*.guest.prx`), `tools/annotate_module.py --out ` does this cut and + disassembly for every executable segment and writes `.asm`, `.imports.txt` and + `.exports.txt`. It names each import call, PLT stub, GOT reference and export entry from the module's NIDs + through the database of `tools/nid_names.py`, which is downloaded on first use unless `--db` names a CSV or + `--no-names` prints the NIDs instead. It needs binutils `objdump`. + A disassembler that loads the program headers (Ghidra, IDA) does the same. 4. Log strings near the call confirm which import it is: `strings -t x eboot.elf`, then the `lea` that loads that address. diff --git a/docs/dev/TRACING.md b/docs/dev/TRACING.md index fa10619b68..51854e22c3 100644 --- a/docs/dev/TRACING.md +++ b/docs/dev/TRACING.md @@ -11,7 +11,7 @@ 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. | diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 43c8c621b4..bcd29e2c3f 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -15,7 +15,6 @@ - [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 @@ -44,7 +43,7 @@ - [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). diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 3337ced18c..8f66ab33c1 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -9,7 +9,7 @@ - [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, 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. @@ -34,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. @@ -52,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 @@ -71,32 +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. -- [agc_driver_frame_replay](../../../core/libs/prx/libSceAgcDriver/CMakeLists.txt) (libSceAgcDriver) is neither built nor registered on Windows until the recorder fix for [#10711](https://github.com/boykopovar/AnyPS5/issues/10711) lands: the recorder reads a delta block with `ReadCommittedUnsynced` while the second DMA copy into it is still in flight, so the capture can hold zeros for that block and the replay does not reproduce the second copy. Configure with `-DANYPS5_RUN_QUARANTINED_FRAME_REPLAY=ON` to build and register it; Linux is unaffected, as the test only exists on Windows. +- [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. +- [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. @@ -112,6 +115,7 @@ - [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. @@ -126,8 +130,9 @@ - [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. +- [Linear color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) with no host import are drawn synchronously (copied in, drawn, waited for, copied back), where the console renders into memory in stream order, unless the draw can make an import that keeps them write-watched. It makes one for a target in registered, writable memory only when the whole import run is write-watched and the import stays watched: a separate alias of shared memory, a direct import of 1 MiB or more of private memory that settles on Windows (when the import probe keeps the watch), or one the probe keeps watched elsewhere. Smaller private ranges on Windows, runs with memory outside the write watch and devices whose imports are compared stay synchronous: a compared import makes every refresh of the resident image compare the whole target. A target over an existing import is resident, compared or not. `APS5_NO_RESIDENT_LINEAR=1` keeps every linear target synchronous. - [`APS5_SKIP_REFUSED_DRAWS`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp) (libSceAgcDriver) drops a draw whose graphics stage the recompiler refuses instead of failing the queue, so the frame lacks that draw, and the stage is compiled and refused again at each such draw. Compute refusals still fail. -- [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), 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 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. @@ -142,8 +147,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. @@ -167,6 +172,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 @@ -178,7 +184,7 @@ - [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces still throw: hardware load bounds and filtered-sample addressing differ. A range of six faces or more that does not end on a whole cube keeps every slice up to `DEPTH`, the bound loads use on hardware; sampling the cube it cuts short is out-of-range cube-array sampling. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Array layer views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (libSceAgcDriver) - `ArrayLayersServeView` lets a 2D texture at a 2D array image's base address, or a 2D array texture over its first layers, view that image (`Texture::CanCopyFrom`). Mesa-built homebrew (ps5-opengl) writes the view's last slice plus one to `CB_COLOR_ATTRIB3.MIP0_DEPTH`, so a slice-0 render target is a two-layer array image, and the title samples it as a plain 2D texture (SuperTuxKart PPSA69421 drew a black 3D scene and minimap without this). The per-layer layout of a mipmapped array is assumed equal to a standalone surface's; it is not measured on the console. A 2D texture at a later slice's address still gets an image of its own. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. -- [Compressed FMASK reads](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) without an exact resident native owner throw. Native reads require eight fragments, one slice and mip, pipe-aligned DCC/CMASK, Z_64KB_X FMASK and unchanged, disjoint watched planes; 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. +- [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. @@ -236,7 +242,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. diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index 5c693e0f85..094bc1d689 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -12,6 +12,7 @@ - [AJM Opus decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. NHL 27 (PPSA34063) passes `1, 48000, 255`. Family 255 with one channel is decoded as one mono stream, the same as family 0; this is assumed, since the stream count, coupled stream count and channel mapping that a family-255 Opus header carries ([RFC 7845 section 5.1.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1.1)) are not passed. Only 48000 Hz, mapping families 0 and 1, and family 255 with one channel are implemented; other rates and families, and family 255 with more channels, throw - [AJM codec 23](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - taken as interleaved `s16` PCM that only passes through the resampler, from Broken Sword - Shadow of the Templars: Reforged (PPSA21480); no source names a codec 23 ([shadPS4's PS4 header](https://github.com/shadps4-emu/shadPS4/blob/5aa7ffa3f4f21bba827d7865ed8f69f98f993542/src/core/libraries/ajm/ajm.h#L19-L25) ends its codec list with `Max = 23`) and the name `CODEC_PCM` is ours. The title decodes its Ogg Opus and Vorbis audio itself and, for every block, initializes the instance (flags 0x1002) with `0, 2, 44100`, sets the ratio 0.91875 (44100/48000) and decodes 235 stereo frames into a 256-frame output. The 12 initialize bytes are taken as input format (0 is `s16`, other values throw), channel count and sample rate; the rate is only reported in the format sideband; other sizes throw; zero or more than 8 channels or a zero rate return the invalid-parameter result as for Opus, not verified. A run consumes its whole input and emits every output frame whose position (output index × ratio) is below the input frame count, with the 4-point Catmull-Rom of the other codecs' resampler and the first and last input frames repeated past the edges, so each run restarts at position 0; `totalDecodedSamples` counts input frames; `s32` and float outputs are accepted although only `s16` was seen. Throwing because the console's behaviour is unknown: a run after a resampled run without an initialize or context clear in between (whether the console carries the resampler position across runs), a ratio change per sample, gapless counts, an empty input or one that is not a whole number of frames, an output that cannot hold the resampled input, run flags other than `SIDEBAND_STREAM` and initialization through `sceAjmBatchJobControl` - [sceAjmBatchJobSetResampleParametersEx](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `(info, instance, float ratio_start, float ratio_change_per_sample, flags, result)` signature is inferred from the name and from `sceAjmBatchJobSetResampleParameters`, and PPSA02225's one call agrees with that layout (`rdi`, `esi`, `xmm0` and `xmm1` as two single floats, `edx`, `rcx`) without showing `flags` or the float roles. With a zero change per sample it sets the ratio as `sceAjmBatchJobSetResampleParameters` does (Watch Dogs: Legion, PPSA01500, calls it so). How a non-zero change ramps the ratio is not confirmed on a console (no source: KytyPS5 ignores it, prosper stubs it, the PS5 firmware does not export the batch builders to a payload): it is assumed to be added to the ratio after each emitted output frame, clamped at zero, and kept across batches. RIDE 4 (PPSA01599) passes one call with a change of 4.6e-7, so the exact rule matters little there. +- [sceAjmBatchJobGetStatistics](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the job zero-fills the first 24 bytes of its sideband and ignores the interval; no source gives this function's result layout or values and no test covers it. [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm.cpp#L973-L979) zero-fills a 48-byte result instead (the 8-byte result, a batch engine usage, three interval engine usages and six memory words), and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ajm/ajm.h#L147-L170) (PS4) names engine and memory statistics sidebands but not this builder - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (bit 13), INITIALIZE (bit 14) and the sideband input order (gapless decode, then initialize parameters) come from [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4); the one call in PPSA25936 passes the flags 0x4000 (bit 14 only), an 8-byte input and an 8-byte output, which agrees with INITIALIZE alone and the 8-byte parameters, without showing the codec or the meaning of the words; other flags throw - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET is assumed to clear the context as sceAjmBatchJobClearContext does - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - initialize parameters are 8 bytes for ATRAC9 and M4AAC, 12 for Opus and none for MP3, as one title passes them; other codecs and sizes, and an output other than the 8-byte result, throw @@ -21,12 +22,13 @@ - [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. @@ -34,3 +36,5 @@ - [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. + +- [AJM three-, four- and five-channel AAC](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp): PS5 PCM equivalence and speaker masks for three and five channels remain unverified. Raw configuration numbers follow the pinned shadPS4 decoder above; PCM follows its FDK-AAC backend's default WAV channel order, documented by [FDK-AAC](https://github.com/mstorsjo/fdk-aac/blob/v2.0.3/libAACdec/include/aacdecoder_lib.h), using FFmpeg's corresponding native layouts. Three- and five-channel format sidebands throw before decoding; format queries after decoding also throw. Four-channel metadata retains the existing source's quad mask even though AAC configuration 4 carries a center and a back-center speaker. Other AAC layouts, including PCE-based configurations and seven/eight channels, still throw. diff --git a/docs/dev/debt/libSceAudio3d.md b/docs/dev/debt/libSceAudio3d.md index 9c17a838c6..a6cd948f49 100644 --- a/docs/dev/debt/libSceAudio3d.md +++ b/docs/dev/debt/libSceAudio3d.md @@ -13,7 +13,6 @@ - [sceAudio3dPortOpen](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - buffer modes 0 and 1 (also selected by the 0x10 and 0x18 parameter sizes) and 3 beds depend on an SDK version check in the module ([prosper](https://github.com/mattias800/prosper)) and throw. A repeated `sceAudio3dInitialize` (`NOT_READY` in shadPS4, 0 in prosper) and `sceAudio3dPortAdvance` on a full queue (`NOT_READY` in shadPS4, unhandled in KytyPS5) throw too - [sceAudio3dPortGetAttributesSupported](../../../core/libs/prx/libSceAudio3d/Export.cpp): the native export validates the port before the count pointer, writes the total count without reading its incoming value, and writes the complete list when an array is supplied. These checks are matched. The list itself remains the host's three port attributes: the native list derives from AudioOut2 device capability masks, SDK version and platform mode, which the host does not yet model. - [sceAudio3dObjectReserve](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF -- [sceAudio3dStrError](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature - [sceAudio3dPortOpenEx](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature - [sceAudio3dPortCreate](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature - [sceAudio3dPortDestroy](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature 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..3f195e8473 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 @@ -31,7 +32,7 @@ - [sceAudioOut2PortGetState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - the port state reports the channel count of the port's data format, output main, volume 127 and a zero reroute counter and flags. Taking the channel count from the data format follows [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs), another emulator. A payload process on a PS5 (firmware 13.60), which is not the foreground application, read output 0, volume -1 and reroute counter 5 for its main ports, and 6 channels for an 8-channel port on a 5.1 system, so a title's values are unverified, including the output of a pad port, the volume after `sceAudioOut2PortSetAttributes` and the reroute counter on a device change. - [sceAudioOut2GetSpeakerInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - The speaker info reports type 0 with available bits `0x3`, the front left speaker at +30 degrees and the front right one at -30, and -360 for the azimuth and elevation of every other slot. On that console the type followed the HDMI device type and the available bits its channels: type 0 with the device set to TV, 1 to AV amplifier and 2 to sound bar, which [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs) names television, AV receiver and sound bar (3 is headphones there), and available bits `0x3` for 2 channels and `0x3F` for 5.1 (centre at 0, surround pair at +110 and -110, -360 for the LFE). - [sceAudioOut2GetSpeakerInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - The front pair and the empty slots read the same in every setting; 7.1 on the AV amplifier reported 2 channels on that console's HDMI chain. The host output is always reported as a stereo television here. The console also refuses flag 1 below a per-process libkernel value whose source is unidentified; BunnyFl measured the boundary between `0x14FFFFF` and `0x1500000`, and it is not modelled here. ASTRO BOT and ASTRO's PLAYROOM reportedly stop on an assertion that depends on these values (#4317), but which field matters has not been isolated -- [sceAudioOutInit and sceAudioOut2Initialize repeats](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - a repeated `sceAudioOutInit` returns 0 here, while the firmware returns `0x8026000e` on the second call; whether a repeated `sceAudioOut2Initialize` returns 0 is unmeasured. That `sceAudioOutInit` initializes AudioOut2, and that `sceAudioOut2GetSpeakerInfo` then succeeds, follow the firmware and are covered by tests. +- [sceAudioOut2InitializeEx and sceAudioOut2LoInit repeats](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - a repeated `sceAudioOut2InitializeEx` or `sceAudioOut2LoInit` returns 0, and either one after `sceAudioOut2Initialize` or `sceAudioOutInit` returns 0 too; the console was not measured for these two. On PS5 firmware 13.60 a repeated `sceAudioOut2Initialize` returns `0x80268004` and a repeated `sceAudioOutInit` `0x8026000e`, and `sceAudioOutInit` after `sceAudioOut2Initialize` still returns 0, which AnyPS5 follows. - [sceAudioOut2UserCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutOpenEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutSetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature @@ -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/libSceCamera.md b/docs/dev/debt/libSceCamera.md index 955b1e157c..cf3e053ff2 100644 --- a/docs/dev/debt/libSceCamera.md +++ b/docs/dev/debt/libSceCamera.md @@ -1,18 +1,17 @@ # libSceCamera technical debt -### Unknown function info - -- [sceCameraOpen, sceCameraClose, sceCameraStart, sceCameraStop, sceCameraGetConfig, sceCameraSetConfig, sceCameraSetAttribute, sceCameraGetDeviceInfo, sceCameraGetFrameData, sceCameraIsValidFrameData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera) - imported by PS4 titles and added without an implementation, declared without parameters. What the console returns from them while no camera is attached is not known, so every call throws. - ### Functional - [sceCameraIsAttached](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera) - no camera is ever attached: index 0 returns 0 (not attached) and any other index returns `SCE_CAMERA_ERROR_PARAM` (0x802E0000), as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/9a8ae4417bbfc79b1a43eb8a2eb0bb602148261e/src/core/libraries/camera/camera.cpp#L187-L200) does without a configured camera, with the error value from its [camera_error.h](https://github.com/shadps4-emu/shadPS4/blob/9a8ae4417bbfc79b1a43eb8a2eb0bb602148261e/src/core/libraries/camera/camera_error.h). Not measured on a console. Closes when: a host camera is exposed to guests. +- [sceCameraOpen, sceCameraClose, sceCameraCloseByHandle, sceCameraStart, sceCameraStartByHandle, sceCameraStop, sceCameraStopByHandle, sceCameraGetFrameData and sceCameraIsValidFrameData](../../../core/libs/prx/libSceCamera/src/Handles.cpp) (libSceCamera) - follow [shadPS4's camera.cpp](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/camera/camera.cpp) with no host camera configured: its check order and 584-byte frame data limit. Handle calls require a live ID; IDs are not reused, unlike shadPS4's counter. The console's ID reuse and exhaustion policy is unknown; exhaustion throws. `sceCameraStartByHandle` succeeds where `sceCameraStart` reports `NOT_CONNECTED`, as in shadPS4; which of the two the console does is not known. The open parameter is not read and no camera memory is mapped. Not measured on a console. Closes when: a host camera is exposed to guests. +- [sceCameraGetAttribute, sceCameraGetAutoExposureGain, sceCameraGetAutoWhiteBalance, sceCameraGetDefectivePixelCancellation, sceCameraGetLensCorrection, sceCameraGetContrast, sceCameraGetSaturation, sceCameraGetSharpness, sceCameraGetHue, sceCameraGetExposureGain, sceCameraGetWhiteBalance and sceCameraGetGamma](../../../core/libs/prx/libSceCamera/src/Attributes.cpp) (libSceCamera) - return the defaults in [shadPS4's camera_config.cpp](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/camera/camera_config.cpp), not values read from a device. The exposure target check from firmware 3.00 always applies. Not measured on a console. Closes when: a host camera is exposed to guests. +- [sceCameraSetAttribute, sceCameraSetAutoExposureGain, sceCameraSetAutoWhiteBalance, sceCameraSetDefectivePixelCancellation, sceCameraSetDefectivePixelCancellationInternal, sceCameraSetLensCorrection, sceCameraSetLensCorrectionInternal, sceCameraSetContrast, sceCameraSetSaturation, sceCameraSetSharpness, sceCameraSetHue, sceCameraSetExposureGain, sceCameraSetWhiteBalance and sceCameraSetGamma](../../../core/libs/prx/libSceCamera/src/Attributes.cpp) (libSceCamera) - report `NOT_CONNECTED` after shadPS4's checks. shadPS4 refuses a non-null exposure gain or gamma in `sceCameraSetExposureGain` and `sceCameraSetGamma`; here a null one is refused, like `sceCameraSetWhiteBalance`. The sharpness limit from firmware 3.50 always applies. Not measured on a console. Closes when: a host camera is exposed to guests. +- [sceCameraGetConfig, sceCameraSetConfig, sceCameraSetConfigInternal, sceCameraGetDeviceConfig and sceCameraGetDeviceConfigWithoutHandle](../../../core/libs/prx/libSceCamera/src/Config.cpp) (libSceCamera) - the type 1 default configuration and the cleared device configurations follow shadPS4's camera_config.cpp and [camera_device.cpp](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/camera/camera_device.cpp); a new configuration is refused with `NOT_CONNECTED` before its type is read, as shadPS4 does without a camera. Not measured on a console. Closes when: a host camera is exposed to guests. +- [sceCameraGetDeviceInfo, sceCameraGetProductInfo, sceCameraGetRegistryInfo, sceCameraGetMmapConnectedCount, sceCameraGetCalibDataFromDevice, sceCameraGetCalibrationData and sceCameraAudioOpen](../../../core/libs/prx/libSceCamera/src/Device.cpp) (libSceCamera) - report a missing device with the errors from shadPS4's camera_device.cpp, camera_config.cpp, [camera_calibration.cpp](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/camera/camera_calibration.cpp) and [camera_audio.cpp](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/camera/camera_audio.cpp). `sceCameraGetCalibDataFromDevice` and `sceCameraAudioOpen` are declared without parameters, as in shadPS4. Not measured on a console. Closes when: a host camera is exposed to guests. ### Additional PS5 3.20 imports -- [sceCameraStopByHandle](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `+X1Kgnn3bzg`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraGetDeviceIDWithoutOpen](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `-H3UwGQvNZI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraGetAttribute](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `0btIPD5hg5A`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraInitializeRegistryCalibData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `0wnf2a60FqI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2GetContrast](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `1-IJHxzRJGw`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2Finalize](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `186Jz869gsc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` @@ -20,57 +19,36 @@ - [sceCamera2IsAttached](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `2v21-m4gljU`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2SetSharpness](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `2zCd8XDOe-Y`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraSetTrackerMode](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `3VJOpzKoIeM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraSetContrast](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `3i5MEzrC1pg`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraGetCalibDataForEve](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `4IGO2LPD3h8`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraGetCalibDataFromDevice](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `5Oie5RArfWs`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2SetContrast](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `66IVWcdNHyI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraIsConnectedForSystem](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `68S3GKWhY6o`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2SetExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `8MEjogxPrv0`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraSetRegister](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `8MjO05qk5hA`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraSetVideoSyncInternal](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `8WtmqmE4edw`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraSetConfigInternal](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `9+SNhbctk64`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraSetAttributeInternal](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `96F7zp1Xo+k`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2SetWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `AfIDd+2ycTs`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraGetMmapConnectedCount](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `B260o9pSzM8`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2GetSaturation](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `DAycoVmY3Mw`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraGetDeviceConfig](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `DSOLCrc3Kh8`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraAudioOpen](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `DhqqFiBU+6g`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2GetAutoExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `FZkD-E6MNTQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraGetCalibDataFromDeviceForEve](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `H-7tWq8gfKs`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraGetWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `HX5524E5tMY`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraGetVrCaptureInfo](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `IAz2HgZQWzE`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraSetCalibDataForEve](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `Jkz3xxMeyIM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2GetAttribute](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `JtD7ZG45rdw`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraSetLensCorrection](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `K7W7H4ZRwbc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraSetCalibData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `LEMk5cTHKEA`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraSetProcessFocusByHandle](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `NVITuK83Z7o`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2SetConfig](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `O5x-G9Rqwx4`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraGetExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `ObIste7hqdk`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraSetSharpness](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `P-7MVfzvpsM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2Initialize](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `PFRl8SmSW+4`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraSetHue](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `QI8GVJUy2ZY`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraAccGetData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `QhjrPkRPUZQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2SetAttribute](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `Qk6kEJ0RJlM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraGetCalibrationData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `RHYJ7GKOSMg`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraGetSaturation](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `RTDOsWWqdME`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2GetHue](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `T8jy0JWa210`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2Stop](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `TZWR3p6XxXk`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2SetSaturation](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `TzkL-nUWfaQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2GetAutoWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `U8oNsRef+jI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraAudioClose](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `UFonL7xopFM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraGetProductInfo](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `ULxbwqiYYuU`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraGetGamma](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `WVox2rwGuSc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraGetLensCorrection](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `XqYRHc4aw3w`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraChangeAppModuleState](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `Y0pCDajzkVQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2GetFrameData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `YlFCMjbyzh8`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraGetContrast](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `a5xFueMZIMs`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraSetSaturation](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `bSKEi2PzzXI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraGetSharpness](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `c6Fp9M1EXXc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2GetExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `c9XZGDF1OcM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraStartByHandle](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `cLxF1QtHch0`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2Open](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `dmLUJh3bVTc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2Start](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `eGkcUia48ts`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraSetLensCorrectionInternal](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `eHa3vhGu2rQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraOpenByModuleId](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `eTywOSWsEiI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2GetCalibrationData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `emZBXv7cuAI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraAudioGetData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `fkZE7Hup2ro`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` @@ -78,41 +56,27 @@ - [sceCamera2GetGamma](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `gJqqsinextg`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraGetCalibData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `hHA1frlMxYE`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraWaitForEveReady](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `hPVradh5wpo`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraGetRegistryInfo](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `hawKak+Auw4`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraAudioGetData2](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `hftC5A1C8OQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraSetAppModuleFocus](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `j5isFVIlZLk`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraSetDefectivePixelCancellation](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `jMv40y2A23g`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraGetDeviceID](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `jTJCdyv9GLU`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraSetForceActivate](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `jeTpU0MqKU0`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraSetWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `k3zPIcgFNv0`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2GetFieldOfView](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `kVWBITsSB3o`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2IsValidFrameData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `lQ7rWWlOArI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraSetProcessFocus](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `lS0tM6n+Q5E`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraSetGamma](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `lhEIsHzB8r4`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2GetConfig](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `mLRzKdIDwiU`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2GetWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `n+R7PGJa6MI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraGetDeviceConfigWithoutHandle](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `n+rFeP1XXyM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraDeviceOpen](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `nBH6i2s4Glc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraSetUacModeInternal](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `nnR7KAIDPv8`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraGetAutoExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `oEi6vM-3E2c`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraGetRegister](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `olojYZKYiYs`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2SetGamma](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `pfiqU2f6PQY`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraRemoveAppModuleFocus](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `py8p6kZcHmA`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraGetAutoWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `qTPRMh4eY60`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraGetDefectivePixelCancellation](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `tslCukqFE+E`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2Close](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `uBRW3tEoWWM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2SetVideoSync](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `ubD+FitjqhA`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraWaitForLukeReady](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `v9XTZHXoJyQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraSetDefectivePixelCancellationInternal](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `vER3cIMBHqI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2GetSharpness](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `vPi3gSzw79M`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2SetAutoWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `vS20kteXHdw`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraSetDebugStop](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `vejouEusC7g`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCamera2SetAutoExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `vx9MQYicBVQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraIsConfigChangeDone](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `wQfd7kfRZvo`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraSetExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `wgBMXJJA6K4`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraSetVideoSync](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `wpeyFwJ+UEI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [sceCameraAudioReset](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `wyU98EXAYxU`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraSetAutoExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `yfSdswDaElo`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraSetAutoWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `zIKL4kZleuc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraGetHue](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `zrIUDKZx0iE`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [sceCameraCloseByHandle](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `ztqH5qNTpTk`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index cda20fb75a..fc6344418b 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -11,7 +11,7 @@ - [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 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) - 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` +- [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 @@ -19,7 +19,8 @@ - [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) - the UTF-8 and UTF-16 buffer forms' errors and size pointers follow the other converters, unverified. The single-character encoders were measured on a PS5 (`sceCesUtf32ToMbc` over U+0000-U+110001 with each MBCS profile, each encoding's `Ucs2To*` and `Utf32To*` on surrogates and a one-byte target): 0x805C0015 for a surrogate, 0x805C0014 past U+10FFFF, the needed length with `DST_BUFFER_END` and a zero length with any other refusal. `sceCesMbcTo*` and `sceCes*ToMbc` convert one character with any profile the MBCS context accepts; an unmapped character answers `UNASSIGNED_CODE`. `sceCesSJisGetCode`/`PutCode` move one raw Shift-JIS code unchecked; `sceCesSJisCodeToJisX0213` and back use JIS X 0213 annex 1 arithmetic. `sceCesMbcsUcsContextInitCopy` copies the profile pointer. Signatures follow Orbital +- [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) - 62 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 and Big5 duplicate choices are assumed; the decode tables 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+2550, U+255E, U+2561, U+256A, U+5341 and U+5345, as code page 950 does. The profiles tag the caller's sheet like the EUC-JP profile +- [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/libSceContentDelete.md b/docs/dev/debt/libSceContentDelete.md index f7365210d5..37b90deeae 100644 --- a/docs/dev/debt/libSceContentDelete.md +++ b/docs/dev/debt/libSceContentDelete.md @@ -7,4 +7,5 @@ ### Unknown function info -- [sceContentDeleteGlobalMstat](../../../core/libs/prx/libSceContentDelete/Export.cpp), `sceContentDeleteMemoryInitialize` and `sceContentDeleteMemoryTerminate`: neither exports nor callers were found in the supplied decompiled library corpus; their signatures remain unresolved. +- [sceContentDeleteByIdListForSf](../../../core/libs/prx/libSceContentDelete/Export.cpp): the shim declares `const std::int64_t*` and `std::size_t` parameters, but neither a matching export nor callers were found in the supplied decompiled library corpus. The body calls `NotImplemented_nid_no_patch` without accessing either parameter. The shim declaration does not establish the console ABI; list semantics, memory access, initialization requirements, results and side effects remain unresolved. +- [sceContentDeleteGlobalMstat](../../../core/libs/prx/libSceContentDelete/Export.cpp), `sceContentDeleteMemoryInitialize` and `sceContentDeleteMemoryTerminate`: neither matching exports nor callers were found in the supplied decompiled library corpus. Their current `int APS5_VABI(void)` shim declarations do not establish the console ABI. The bodies call `NotImplemented_nid_no_patch` without accessing arguments or state; signatures, lifecycle requirements, results and side effects remain unresolved. 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 57d9015321..117cf2250f 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -6,13 +6,15 @@ - [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. +- [sceFontRenderCharGlyphImage, sceFontRenderCharGlyphImageHorizontal, sceFontRenderCharGlyphImageVertical](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - four titles disassembled for this entry import them: PPSA01530 (CharGlyphImage, 2 call sites), PPSA13356 (CharGlyphImage, 4), PPSA15246 (Horizontal, 3) and PPSA14421 (Horizontal, 1); none imports Vertical. PPSA13356 initialises two surfaces of 64 x 64 and 1920 x 1080 at 1 byte per pixel (0xba084, 0xba5e8) and calls again with the character '_' (0x5f) at 0xba111 when the first call (0xba0e2) returns nonzero; PPSA15246 (0x229413) and PPSA14421 (0x1054d8) do not test the result. The extents of the other surfaces are run-time values, so whether any title passes 0 is not shown. + - [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 @@ -21,7 +23,7 @@ ### 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 @@ -31,7 +33,7 @@ - [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. @@ -44,3 +46,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 f52d05e5d0..205ee0df69 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -1,6 +1,8 @@ # 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 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/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..633c67724e 100644 --- a/docs/dev/debt/libSceHttp2.md +++ b/docs/dev/debt/libSceHttp2.md @@ -1,19 +1,22 @@ # 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 and requires at least 64 KiB. On 13.42 the pool is rounded up to 16 KiB (wrapping to 0 for `SIZE_MAX`) and mapped with `sceKernelMapNamedFlexibleMemory`, whose error is returned unchanged; the host maps it the same way under an assumed name, 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. `sceHttp2CookieFlush` empties a cookie box and answers 0 for any other id. Nonzero cookie-box binding by `sceHttp2SetCookieBox` throws +- [libSceHttp2 cookie store](../../../core/libs/prx/libSceHttp2/Cookies.cpp) (libSceHttp2) - behavior follows a run of `GuestHttp2Cookies.cpp`'s sequence against 13.42's `libSceHttp2.sprx` ([#10534](https://github.com/boykopovar/AnyPS5/pull/10534)). Not reproduced: the `0x817B1076` stack check below 0x1FD0 bytes of stack, and the pool size is not enforced. Assumed, not measured on libSceHttp2: the default total limit of 3000 with the 10 oldest dropped past it, and the NUL, space, NUL written after the header (both libSceHttp's), which of no match and null `cookie`/`required` is checked first, public-suffix rules for `Domain`, and dates beyond the tested ones. Equal-length paths come out with the first stored last, a rule inferred from the runs -- [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). +- [libSceHttp2 pending exports](../../../core/libs/prx/libSceHttp2/Export.cpp) - 12 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 265fb144aa..b49e7d07ec 100644 --- a/docs/dev/debt/libSceHttpCache.md +++ b/docs/dev/debt/libSceHttpCache.md @@ -3,4 +3,10 @@ ### Unknown function info - [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. -- 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), [`sceHttpCacheRegisterCacheInfoUrl`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheRemoveTag`](../../../core/libs/prx/libSceHttpCache/Export.cpp). +- [`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/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/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index bd201c0765..723c8f3d9d 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,19 +2,24 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 342 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 319 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [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 +- [RSA_flags, RSA_size](../../../core/libs/prx/libSceLibreSsl/src/Rsa.cpp) (libSceLibreSsl) - nothing is checked on the console. Layouts and results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto) `rsa.h`, `rsa_crpt.c` and `bn_lib.c`, the version in the [firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). No export of this module creates a key yet, so only keys the caller builds reach them. `RSA_size` counts the modulus bytes from its top word, as `BN_num_bytes`, so a zero top word is not skipped. A null method, a null key or modulus, and a positive word count with null words, which LibreSSL dereferences, throw, as do a negative word count and a bit count that overflows `int` ### 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 c98952a914..fb9e7fe201 100644 --- a/docs/dev/debt/libSceLibreSsl3.md +++ b/docs/dev/debt/libSceLibreSsl3.md @@ -2,9 +2,10 @@ ### Unknown function info -- [libSceLibreSsl3 pending exports](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) - 84 named callable exports, excluding `CRYPTO_memcmp`, `MD5` and `SHA1`, remain unimplemented from the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl3.c), cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl3 pending exports](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) - 83 named callable exports, excluding `CRYPTO_memcmp`, `MD5`, `SHA1` and `d2i_ASN1_INTEGER`, remain unimplemented from the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl3.c), cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional +- [ASN.1 integer decoding](../../../core/libs/prx/libSceLibreSsl3/src/IntegerDecoder.cpp) throws on rejected encodings/allocation failures instead of matching native NULL/error handling. Redundant-sign/empty-content rejection retains old output that the module frees and clears. The decoder does not populate the legacy queue; remaining LibreSSL3 ERR exports throw. [13.42 native-PC evidence](https://github.com/boykopovar/AnyPS5/pull/10948#issuecomment-6412137728), using a recording allocator, confirms tested replacement/free order, allocation cleanup and negative-length NULL returns. Nonnegative null input faults natively and throws here. Console/application-heap ABI and untested error-precedence/overflow combinations remain unverified. Contracts follow [LibreSSL 4.0.0](https://github.com/libressl/openbsd/tree/ce488b78e0bd994780569df718596b9776ff681a/src/lib/libcrypto/asn1). Closes when: error/replacement semantics and console/allocator interoperability are verified. - [CRYPTO_memcmp](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) follows the public [LibreSSL signature and equality contract](https://man.openbsd.org/CRYPTO_memcmp.3), without PS5 verification. The exact nonzero result for unequal buffers is unspecified by that contract. Content-independent execution of its volatile byte reads still depends on the compiler; inspect optimized code when changing toolchains. Closes when: measured on a PS5 and the supported toolchains' generated code is checked. - [MD5 and SHA1](../../../core/libs/prx/libSceLibreSsl3/src/Digest.cpp) (libSceLibreSsl3) - follow LibreSSL's [md5.c, sha1.c](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto) and hash through libSceLibreSsl's `MD5_*` and `SHA1_*`. A null digest buffer, which LibreSSL 4.0.0 does not check, throws. Nothing is checked on the console 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 b82106a11b..2d8f60dd98 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -21,8 +21,10 @@ - [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 +- [sceNetInitParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - [measured on firmware 13.42 `libSceNet.sprx` by BunnyFl](https://github.com/boykopovar/AnyPS5/pull/10881#issuecomment-6397362559): a null parameter returns `0x80410116`, the first non-null call copies its first 32 bytes and returns 0, and every later call returns `0x80410110`, after `sceNetTerm` too. Of the copy the module reads only bit 0 of the first word, which raises `sceNetPoolCreate`'s limit from 32 pools (IDs 3000 to 3031) to 128 (3000 to 3127), with `0x80410117` past it; that limit is not modelled, as `sceNetPoolCreate` here has none. The first successful call also marks the library initialized, as `sceNetInit` does here, where the other calls check it; on the console `sceNetInit` and `sceNetTerm` only return 0 - [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 - [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the native routine consumes no arguments; the throwing implementation remains unchanged. +- [sceNetConfigGetIfFlags](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - interfaces 1 to 9 throw: the console answers them from the kernel, and `eth0` needs the host interface only libSceNetCtl tracks. `lo0` (0) reports `IFF_UP | IFF_LOOPBACK | IFF_RUNNING | IFF_MULTICAST` (0x8049), the flags FreeBSD 11 gives a configured loopback (`sys/net/if_loop.c`, combined as `SIOCGIFFLAGS` does in `sys/net/if.c`); unverified on console. A number of 10 or more returns -1 with the flags untouched, and a null flags pointer returns `0x8041130E` before anything else, as firmware 13.42's module does ([measured by BunnyFl](https://github.com/boykopovar/AnyPS5/pull/10881#issuecomment-6397362559)); the errno left after its failed `SIOCGIFFLAGS` isn't modelled. Calls before `sceNetInit` are answered the same way, also unverified - [sceNetDhcpdStart](../../../core/libs/prx/libSceNet/Export.cpp): six arguments and the three-argument address callback are confirmed. The 24-byte parameter contains five network-order addresses and a client limit. Individual address roles and the two unused thread-option meanings remain unresolved. - [sceNetPppoeStart](../../../core/libs/prx/libSceNet/Export.cpp): seven arguments are confirmed, including the parameter pointer on the stack. The 272-byte parameter contains two DNS addresses, two 128-byte credential arrays, MTU and flags. Callback arguments are user data, event and error; the two unused thread-option meanings remain unresolved. - [sceNetConfigRoutingStart](../../../core/libs/prx/libSceNet/Export.cpp): five pointer/scalar arguments are confirmed. Input address data is eight bytes; the mutable server parameter is 20 bytes and duplicates its first address when its count is one. Optional rules contain a count and pointer to 20-byte records with protocol, two address/port pairs and flags. Layouts are defined; rule flag meanings and protocol-specific port interpretation remain unresolved. @@ -30,6 +32,8 @@ ### Functional +- [IPV6_V6ONLY in sceNetSetsockopt and sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) is unverified on PS5. The IPv6 level (41), option (27), exact integer input size, nonzero enable behavior, boolean readback and rejection after binding follow FreeBSD 11.4's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet6/ip6_output.c). The shared libkernel option implementation applies the host's IPv6-only mode; disabling it permits IPv4-mapped traffic on a socket bound to the IPv6 wildcard address. Host network configuration controls IPv6 availability. Null buffers retain this library's `EINVAL` behavior instead of FreeBSD's fault handling. + - [sceNetRecv/sceNetRecvfrom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; the console measurement covered only libkernel_sys `recv` and `recvfrom`, not libSceNet. - [sceNetUsleep](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - whether the console prints its `[LIBNET] usleep ` line for a negative count before `sceNetInit` is unmeasured; nothing is printed here. What an interrupted sleep returns is unmeasured: AnyPS5 returns the result of libkernel's `sceKernelUsleep`, a cancellation point. The signed 32-bit count (the upper half of the register is ignored) and the 0 result were measured on a PS5 (firmware 13.60) - [sceNetIoctl](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - on a `sceNetSocket` socket only `FIONBIO` and `FIONREAD` are handled and other requests fail with `ENOTTY`, as in libkernel's `ioctl`, though FreeBSD 11.4's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L156-L262) handles more; not measured on a console. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/60c42b925c3d1e00008621b8587819999260ec31/src/core/libraries/net/net.cpp#L1839) (PS4) returns `EINVAL` for other requests. `FIONBIO` sets `SO_NBIO`, assumed to be `soo_ioctl`'s `SS_NBIO`. `FIONREAD` asks the host, which on Linux counts only the next datagram where `soo_ioctl` counts all. Libkernel's own sockets go to its `ioctl`; other descriptors get `EBADF`. @@ -39,21 +43,27 @@ - [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. +- [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. +- [MSG_NOSIGNAL](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - `sceNetSend`, `sceNetSendto` and `sceNetSendmsg` accept `0x20000`, FreeBSD's `MSG_NOSIGNAL` (`sys/sys/socket.h`), which ProsperoTV (PPSA99003) passes on every phone-remote reply. Firmware 13.42's `libSceNet.sprx` adds `0x40020000` to the flags of every send and refuses no flag itself ([measured in an emulator](https://github.com/boykopovar/AnyPS5/pull/11037#issuecomment-6400759240)), so passing the flag changes nothing on the console. What `0x40000000` means and which flags the kernel refuses are unknown; other flags still fail with `EOPNOTSUPP` here. Host sends outside Windows already pass `MSG_NOSIGNAL` and Windows raises no `SIGPIPE`, so a send on a stream whose write side is shut down fails with `EPIPE` +- [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 b245be45b0..3f50d7f70a 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,7 +16,7 @@ - [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 `sceAudioOut2PortCreate` entries in [libSceAudioOut](libSceAudioOut.md), 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 @@ -24,19 +26,19 @@ - [NGS2 sampler voice state flag EMPTY](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a playing sampler voice that still accepts blocks (`CONTINUE`) and has played all queued blocks reports `INUSE|PLAYING|EMPTY` (0x20) and keeps playing, so blocks added later play on. PPSA18391 (engine: Clickteam Fusion) reads `state_flags & 0x22 != 2` from `sceNgs2VoiceGetState` as the end of a sound and only then reuses the voice. When the console sets and clears EMPTY, and whether it also reports it for other voice types, is unverified on hardware. - [NGS2 reverb channel conversion](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native) - a reverb voice whose `num_input_channels` differs from `num_output_channels` (PPSA03747 sets up 2 inputs and 8 outputs) mixes its ports in the input layout and folds that to mono with the input count's gains, as an equal-count voice does, and runs the comb and allpass stages per output channel. The PS4 library's handling of the dry signal in this case is not checked: here it reaches the output layout through the default channel map, so a stereo input keeps its dry signal on the front left and right and the other outputs carry only the reverb - [NGS2 port matrix](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a port without a matrix, or with a selected matrix whose levels were never set, mixes through a default channel map: mono reaches both stereo channels at -3 dB and the 5.1 and 7.1 centre, stereo folds to mono at -3 dB per side, and wider sources fold down with 0.5 and -3 dB gains. The map and the equivalence of an unset matrix with no matrix were measured from the PS4 firmware 11.00 `libSceNgs2.sprx`, not yet from the PS5 library. That library only accepts submixer and mastering voices with 1, 2, 6 or 8 channels, so the map into 3, 4, 5 or 7 channels throws. A matrix set with fewer levels than the destination needs still mixes only the rows it has -- [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - UserFx2 process flags and setup/process/cleanup order follow only [KytyPS5](https://github.com/KytyPS5/KytyPS5), not a console measurement; layouts follow [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`. A module control handler, unequal input and output channels, setup flags and default rack options throw. Module state is a separate buffer, not the `state_offset` slice of voice state; `sceNgs2VoiceGetState` throws for this rack. UserFx2 runs only on grains with input samples, so an effect tail stops with the input +- [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - UserFx2 process flags and setup/process/cleanup order follow only [KytyPS5](https://github.com/KytyPS5/KytyPS5), not a console measurement; layouts follow [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`. A module control handler, unequal input and output channels, setup flags and default rack options throw. Module state is a separate buffer, not the `state_offset` slice of voice state; `sceNgs2VoiceGetState` throws for this rack. UserFx2 runs only on grains with input samples, so an effect tail stops with the input. Setup changes from custom process callbacks throw before later modules can use the changed layout; inconsistent input-mixing layouts after callbacks also throw. Console reentry behavior is unknown - [NGS2 custom UserFx](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - the 48-byte process context, process flags, callback error handling and null-handler behavior retain the existing submixer callback model and are not console-verified. Non-null module options throw because their console error is unknown. UserFx on custom sampler rack 0x4001 throws: its parameter indexing and routing are not implemented. Module id 0x19, the 40-byte voice parameter (handler and three user-data words), parameter id `0x40001900 | index`, and lack of option and module state follow [fma-rc's PS5 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943), including parameter dispatch at 0x25ec4. - [NGS2 custom Delay](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, 0x1d) - processing and defaults remain unverified on PS5. The [agreed compatibility path](https://github.com/boykopovar/AnyPS5/pull/9337#issuecomment-6351205140) preserves main's PCM pass-through for Void Terrarium (PPSA03061) until any Delay parameter is set; every `0x40001dxx` parameter throws. Only one-buffer custom submixers with no module state and a 20-byte option are accepted. Option type 0/1, taps 1..8, length 1..20000 and reserved 0 follow [fma-rc's 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943). Other shapes and values throw because their console errors are unknown. This preserves host behavior, not console-equivalent Delay processing -- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - Reverb (0x1b), PitchShift (0x20) and Convolution (0x23) are not processed: a rack that passes their creation checks throws. Gain (0x15) on custom sampler racks also throws. Per [fma-rc's 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A8302` for a wrong non-zero state size, and `0x804A8013`, `0x804A8099` and `0x804A8098` for a bad Reverb option size or word and PitchShift word. Unknown: check order, errors for a Gain option, missing Reverb or PitchShift options, other PitchShift and Convolution option sizes, and Convolution's state (64 bytes from SDK 3.0). PPSA04540 uses 0x15, 0x19, 0x1b and 0x23 (#8039), PPSA35169 0x20 (#4246) and PPSA33655 0x15, 0x19 and 0x1b -- [NGS2 custom Gain module](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - follows [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943): parameter `0x40001500 | module index` holds 37 linear levels, one per channel, default 0, and the module writes source × level into dest, ramping linearly over the grain from the previous level. Unverified: where the ramp starts and ends (here the grain's first sample is one step past the previous level and its last sample is at the new level), whether stopping or setting up the voice again resets the levels, and what the library does with a level outside -32 to 32 (here it throws). On a custom sampler rack, whose parameter index is one higher per fma-rc, Gain throws. A Gain module with an option throws: fma-rc reads Gain as taking none, and the error the library returns for one is unknown. The extra buffer is ignored, as fma-rc reads it +- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - Reverb (0x1b), PitchShift (0x20) and Convolution (0x23) are not processed: a rack that passes their creation checks throws. Gain (0x15) on custom sampler racks still throws: its processor initialization and lifecycle are unverified. Per [fma-rc's 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A8302` for a wrong non-zero state size, and `0x804A8013`, `0x804A8099` and `0x804A8098` for a bad Reverb option size or word and PitchShift word. Unknown: check order, errors for a Gain option, missing Reverb or PitchShift options, other PitchShift and Convolution option sizes, and Convolution's state (64 bytes from SDK 3.0). PPSA04540 uses 0x15, 0x19, 0x1b and 0x23 (#8039), PPSA35169 0x20 (#4246) and PPSA33655 0x15, 0x19 and 0x1b +- [NGS2 custom Gain](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) - PS5 rounding and first-parameter processor state are unverified. The initial previous gain retains main's zero convention; the schema default does not verify processor initialization. Processing before a parameter, after re-setup or after stopping a configured voice throws. The unclamped levels, i/N ramp and equal-gain paths follow [the PS5 12.70 reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6391072574) (0x33ce0, 0x5310); invalid parameter positions return 0x804A835A (0x250b0). Custom sampler Gain and module options throw: their initialization, lifecycle and option errors remain unknown. Integer rendering of nonfinite samples throws because the console conversion is unknown. - [NGS2 custom mastering rack](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native, rack 0x4003) - follows [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943): option size is 0x508, matching the custom submixer rack layout. Creation returns `0x804A8052` above 37 channels; channels 9 to 37 throw because voices hold at most 8 channels. Mastering racks with modules throw: attached modules and non-default option fields are unverified on PS5. Voice control parameters 0x40030000 (setup), 0x40030004 (gain) and 0x40030005 (output) configure channels, levels and destination buffer. `sceNgs2VoiceGetState` fills common voice state flags. PPSA10278 and PPSA04540 create rack 0x4003 without modules (#8039) -- [NGS2 custom rack buffers and channels](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, racks 0x4001 and 0x4002) - racks with 2 to 4 buffers, custom submixers with 9 to 37 channels and submixer `max_inputs` other than 1 or 2 throw: how modules and ports route several buffers is unknown, and voices hold at most 8 channels. Per [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A830B` for a buffer count outside 1 to 4 or below the submixer's `max_inputs`, `0x804A8306` for a module source, extra or dest buffer id at or above the count, and `0x804A8052` for more than 37 submixer channels; the check order is not known. `sceNgs2RackQueryBufferSize` throws for these options, as no source says whether it checks them. A port buffer id other than 0 throws. PPSA04540 creates racks with up to 4 buffers and a 16-channel custom submixer (#8039) +- [NGS2 custom buffers](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) - multiple buffers on samplers, submixer channels above 8, inputs beyond 0/1 and multi-buffer chains beyond zero modules or one Gain throw. Default custom module order, UserFx extra-buffer selection and invalid port-buffer errors remain unknown. Input-to-buffer routing and grain clearing follow [the PS5 12.70 reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6391072574) (0x181a0); output-port selection follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/83f8bfd3c9c35fcb135ef6c33dfd88bbfe066ee2/src/libs/ngs2.cpp#L417-L419). These paths have not been compared with console output. Creation validation order and QueryBufferSize validation remain unverified; the query throws for options whose creation returns an error. - [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts - [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) keep a registry of report handlers. Register rejects a null handler with an invalid argument and returns `INVALID_OUT_ADDRESS` for a null out handle, then stores the report type, handler and user data under a freshly allocated opaque handle; unregister removes and frees the matching record and throws for a handle that is not registered. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`, and the 24-byte `Ngs2ReportDataHeader` passed to a handler is its `OrbisNgs2ReportDataHeader`. No report is produced yet, so no registered handler is ever invoked and PPSA04540, which imports both and registers a CPU load handler (type 5), still receives none. - [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,8 +46,8 @@ - [sceNgs2VoiceGetOwner](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the signature `(voiceHandle, outOwnerRack, outVoiceIndex)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L439-L440) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.cpp#L322-L326), which only logs it; neither gives an error. The errors are unmeasured: a handle that is not a live voice answers `INVALID_VOICE_HANDLE` before the outputs are checked, as in `sceNgs2RackGetVoiceHandle`, and either output pointer null answers `INVALID_OUT_ADDRESS` without writing the other - [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 +- [NGS2 patch destinations](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) - a rack handle with input 0 resolves to its first voice, matching the Dead Cells call in #4334; selection on multi-voice racks remains unverified. Input 1 is supported only for a voice handle in a two-input custom submixer. Nonzero rack-handle inputs, other unsupported inputs and nonzero inputs on disconnected ports throw because their contracts are unknown. GetPortInfo reports the resolved voice handle and selected input, with zero delay. Changes to a port destination or input while its source is rendered throw before mixing: callback routing order is unverified, and combining the old destination with the new input is invalid. +- [NGS2 PCM waveform parsing](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - `sceNgs2ParseWaveformData` reads integer PCM (tag 1, or `WAVE_FORMAT_EXTENSIBLE` with the PCM GUID) of 8, 16, 24 and 32 bits as types 0x11, 0x12, 0x14 and 0x16, and float (tag 3) of 32 and 64 bits as 0x18 and 0x1a, and returns 0x804A8432 for other depths, after [prosper's reading of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L194-L223); not measured on a console. Every depth keeps the checks of the 16-bit path (channels, rate, block align, whole frames, a looped `smpl` chunk throws), which that reading does not apply - [sceNgs2ParseWaveformFile](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - inputs on which the PS5 `libSceNgs2.native.sprx` of firmware 3.20 and 4.03 did not return within 60 s throw; whether it returns at all for them is unknown. They are an infinite speaker or param angle, an angle too large for a turn to change it, a speaker angle that wraps to exactly the unit angle, and a param angle that wraps onto the first speaker plus one turn, such as one float below the first speaker in radians 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/libSceNpCppWebApi.md b/docs/dev/debt/libSceNpCppWebApi.md index ee3994c483..e796fc8e2f 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 @@ -18,3 +18,4 @@ - [sce::Np::CppWebApi::UserProfile::V1, ProfanityFilter::V2, CommunicationRestrictionStatus::V3](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by an Epic Online Services module whose dump ships no `libSceNpCppWebApi.prx`. The reference-count offset 0x00 of `ResponseHeaderBase` and `WebApiFilterRequest` is assumed from the other objects, not measured. Offline, the five API calls, `WebApiFilterRequestFactory::create` and the transactions' `start`/`getResponseHeaders` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`. Response accessors, `ResponseHeaderBase::getHeaderValue` and the `Vector`/`Iterator` members over `IntrusivePtr` and `unsigned long` throw: a failed request never produces a response - [libSceNpCppWebApi](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - unimplemented stubs have their parameter lists, const qualifiers and return types aligned with the demangled symbols confirmed by NID matching in [#8143](https://github.com/boykopovar/AnyPS5/issues/8143). They cover response-body accessors, iterators, and uninstantiated transactions in Leaderboards::V1, SessionManager::V1, InGameCatalog::V5, TitleCloudStorage::V1, and Common templates: each mangled name gives the method and parameter types, but the runtime layout of response objects and errors are unverified on console, so each call continues to throw NotImplemented_nid_no_patch. No call site is recorded for them - [sce::Np::CppWebApi::SessionManager2V1 and sce::Np::CppWebApi::InGameCatalog2V5](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 198 further exports imported by PPSA20397, named from aerolib. API calls, factories and transaction `start`/`getResponse*` fail `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter bookkeeping succeeds without state; `IntrusivePtr`/`Vector` helpers follow the layouts above, with `Vector::empty`/`size`/`operator[]` reading host-created bodies and throwing on title-provided ones. Response accessors, iterators, `Vector::begin`/`end`/`copyFrom` and transaction `abort`/`getId`/`operator=` throw: a failed request produces no response object, and aborting or renumbering a transaction that never ran has no defined outcome +- [sce::Np::CppWebApi::UserProfile::V1, sce::Np::CppWebApi::CommunicationRestrictionStatus::V3 and the remaining sce::Np::CppWebApi::Common instantiations](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 87 further exports imported by PPSA20397, plus the previously-throwing `ContainerApi::ParameterToGetContainer` lifecycle and `getContainer`. API verbs and transaction `start`/`getResponse` fail `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter and model bookkeeping (`initialize`, `terminate`, `set*`, constructors, destructors) succeeds without state. `BasicProfile`/`BasicPresence`/`PersonalDetail` response accessors, `*IsSet` queries, `Get*Response` getters, vector `begin`/`end`, iterator dereference/compare and transaction `abort`/`getId`/`operator=` throw: no response object exists offline and the model layouts are unverified diff --git a/docs/dev/debt/libSceNpGameIntent.md b/docs/dev/debt/libSceNpGameIntent.md index 622b40a2c2..a912f37f7e 100644 --- a/docs/dev/debt/libSceNpGameIntent.md +++ b/docs/dev/debt/libSceNpGameIntent.md @@ -3,10 +3,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 +- [sceNpAppLauncherInitialize, sceNpAppLauncherTerminate](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - app-launcher lifecycle returns success without performing launch work; `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; an atomic flag tracks initialized state so `sceNpAppLauncherLaunchApp` can reject calls made outside the lifecycle ### Unknown function info -- [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. +- [sceNpGameIntentReceiveIntent, sceNpGameIntentGetPropertyValueString](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - firmware 13.42 traces report `0x80553802` before `sceNpGameIntentInitialize` (precedence over argument validity), and `0x80553804` for invalid arguments (null pointer, empty key, or zero buffer size); `sceNpGameIntentTerminate` resetting initialized state is modeled lifecycle behavior rather than hardware-verified. `sceNpGameIntentReceiveIntent` reportedly requires `info->size == 0x4140` on firmware 13.42, while AnyPS5 `NpGameIntentInfo` is `0x560` bytes (matching fpPS4) and set by `sceNpGameIntentInfoInit`, so initialized objects are rejected by the receiver's measured size guard. Complete retail layout, output field offsets, and retail behavior of `sceNpGameIntentInfoInit` are unverified; expanding the type with unverified reserved bytes is not an evidence-backed solution. +- [sceNpAppLauncherLaunchApp](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent, NID `GbayQ7DO8jA`) - its recovered signature is `(int32_t, const char*, const char*)`: an unhandled first argument, a required non-empty string and an optional string rejected past 0x2000 bytes, returning `0x80553A02` before `sceNpAppLauncherInitialize` and `0x80553A04` on invalid arguments. The launch itself is not implemented and throws; the firmware tail calls an internal launch through an extra library context and remaps its `0x805538xx` errors into `0x80553Axx` +- [sceNpGameIntentGetUdsNpComId, sceNpGameIntentLaunchApp, sceNpGameIntentLaunchApp2, sceNpGameIntentNotifyAppLaunched, sceNpSessionServiceGetJoinedPartySession, sceNpSessionServiceGetRunningGamePlayerSession and sceNpSessionServiceSendPlayerSessionInvitations](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpGameIntent.c). They are declared without parameters solely to resolve known imports and make no ABI claim. - [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..e085e956ef 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,14 @@ - [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 before the library's NP service client exists from `sceNpManagerIntCreateRequest`, `sceNpManagerIntDeleteRequest` and `sceNpManagerIntAbortRequest` ([measured on firmware 13.42 `libSceNpManager.sprx` by BunnyFl](https://github.com/boykopovar/AnyPS5/pull/10879#issuecomment-6397362281)) and, before any argument check, from the eleven NpManagerInt getters ([the #10890 measurement](https://github.com/boykopovar/AnyPS5/pull/10890#issuecomment-6397162960)), 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) +- [sceNpManagerIntGetAccountAge, sceNpManagerIntGetAccountCountryA, sceNpManagerIntGetAccountLanguageA, sceNpManagerIntGetNpId, sceNpManagerIntGetOnlineId](../../../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/10890#issuecomment-6397162960): user id -1 or a null output gives `SCE_NP_ERROR_INVALID_ARGUMENT`, and `GetAccountAge` and `GetAccountLanguageA` look their request id up only after the checks. Past them the call 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, for any request id, since the request lookup's own answer is not measured +- [sceNpManagerIntGetUserIdByOnlineId, sceNpManagerIntGetUserIdByNpId](../../../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/10890#issuecomment-6397162960): a null input or output gives `SCE_NP_ERROR_INVALID_ARGUMENT`. Past them the answer follows main's offline model of `sceNpGetUserIdByOnlineId`, not a console measurement: `SCE_NP_ERROR_SIGNED_OUT`, writing nothing +- [sceNpManagerIntAuthGetAuthorizationCode, sceNpManagerIntAuthGetIdToken, sceNpManagerIntGetAuthorizationCode, sceNpManagerIntGetAuthorizationCodeWithPsnoUri](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the arities and checks are [measured on firmware 13.42 `libSceNpManager.sprx` by BunnyFl](https://github.com/boykopovar/AnyPS5/pull/10890#issuecomment-6397162960), with no libSceNpAuth layout: user id -1 or a required pointer null gives `SCE_NP_ERROR_INVALID_ARGUMENT`, as do the eight-argument call's values outside 0 to 3 and 1 to 3; a mode other than 0, 3 or 5 gives `0x80550004`; the URI form refuses a size outside 1 to 0x801 with `0x80550011`, then the mode, then a URI of 0x2000 bytes or more with `0x80550005` (terminator unmeasured). Past the checks the answer follows main's offline model, not a console measurement: `SCE_NP_ERROR_SIGNED_OUT`, writing nothing, for any request id; the request lookup and the URI form's `sceHttpUriParse` step are not modelled +- [sceNpManagerIntGetAccountId, sceNpManagerIntGetAccountIdSdk, sceNpManagerIntGetUserIdByAccountId](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - still throw: their answer or their input is an account id, 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), as for `sceNpIdMapperAccountIdToNpId` - [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..f370687fb1 100644 --- a/docs/dev/debt/libSceNpSessionSignaling.md +++ b/docs/dev/debt/libSceNpSessionSignaling.md @@ -9,4 +9,6 @@ ### 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 + +- [sceNpSessionSignalingGetGroupFromSessionId, sceNpSessionSignalingGetConnectionFromPeerAddress, sceNpSessionSignalingGetConnectionFromPeerAddress2](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - three titles disassembled for this entry import them, five call sites in all: PPSA02530 (GroupFromSessionId 0x87c9118, PeerAddress2 0x87c912f), PPSA02225 (PeerAddress2 0x9e5595 and 0x9e5787) and PPSA01285 (PeerAddress 0xeecf55). PPSA02530 and PPSA01285 test the result only for negative, PPSA02225 at 0x9e5787 only for nonzero, and none of these sites compares it with a specific code; on a negative result PPSA02530 jumps to 0x87c914d, and on a nonzero result PPSA02225 jumps to 0x9e53d0 at 0x9e5787. What PPSA01285 does after a negative result (a wait path from 0xeecf9b) was not read. 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/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/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 911bec0271..5c75033c4d 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -11,7 +11,7 @@ - [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. `scePadOpenExt` throws for other port types 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 9c664a7afc..a8967ae3cd 100644 --- a/docs/dev/debt/libScePosixForWebKit.md +++ b/docs/dev/debt/libScePosixForWebKit.md @@ -4,14 +4,16 @@ - [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 + +- [getaddrinfo raw-socket hints](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) follow FreeBSD 11.4's [raw exploration entries and service rejection](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/net/getaddrinfo.c#L173-L196): an explicit raw type preserves any protocol number and forbids a non-null service. Address lookup uses the host's datagram resolver path with no service, then produces guest raw records with zero ports. PS5 execution is unverified; host DNS policy, address ordering, default-hint records and flag behavior can still differ from FreeBSD. diff --git a/docs/dev/debt/libScePsml_debug.md b/docs/dev/debt/libScePsml_debug.md index b8bdedf383..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. +- [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`; 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 +- [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. The console's library fails there instead: in firmware 13.60 (read from disassembly, not measured on a console) `scePsmlMfsrInit` returns 0x8A810012 when `sceKernelIsTrinityMode` returns 0, so its later MFSR calls return 0x8A810001. 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..d085c6eea7 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -9,13 +9,12 @@ - [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 - [sceRtcSetCurrentTick, sceRtcSetCurrentNetworkTick, sceRtcSetCurrentAdNetworkTick, sceRtcSetCurrentDebugNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a valid tick sets a process-local clock that advances from a monotonic host clock; the host system clock is not changed. The console sets clock 0, 0x10, 0x12 or 0x11. Null pointers return `0x80B50002` and ticks before the Unix epoch return `0x80B50003`, from disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console. sceRtcSetConf passes its third and fourth arguments to the same clock call with clock 0 and throws; what the pair selects is unknown - [sceRtcFormatRFC3339Precise, sceRtcFormatRFC3339PreciseLocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - read from the disassembly of libSceRtc.sprx on firmware 13.60: a null tick means the current tick, a precision above 6 or a zone outside -1439 to 1439 minutes returns `0x80B50003`, and the fraction is the microseconds truncated to the requested number of digits. Not compared with console output. The output buffer size is unknown; at most 33 bytes are written -- [sceRtcGetYear, sceRtcGetMonth, sceRtcGetDay, sceRtcGetHour, sceRtcGetMinute, sceRtcGetSecond, sceRtcGetMicrosecond, sceRtcSetYear, sceRtcSetMonth, sceRtcSetDay, sceRtcSetHour, sceRtcSetMinute, sceRtcSetSecond, sceRtcSetMicrosecond](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - inferred component behavior without confirmed hardware trace. Component setters validate individual boundaries (e.g. day 1..31) independently without cross-field calendar validation; getters return `0x80B50002` (`SCE_RTC_ERROR_INVALID_POINTER`) for a null pointer ### Functional diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index 6b667bd48d..31b53a4283 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -11,5 +11,6 @@ - [sceRudpGetStatus](../../../core/libs/prx/libSceRudp/Export.cpp) - PPSA01521 passes an output pointer and byte count `0xF8`. Measured on a PS5 (firmware 13.60): the status is 0xF8 bytes; before `sceRudpInit` every call returns `RUDP_ERROR_NOT_INITIALIZED`, and after it a null pointer or a byte count of 0 or above 0xF8 returns `RUDP_ERROR_INVALID_ARGUMENT`. The layout matches `SceRudpStatus` in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp) with `currentContexts` at 0x64 and `allocs`, `frees`, `memCurrent` and `memPeak` at 0xC8 to 0xD4. Only `currentContexts` is reported: the traffic counters stay 0 because nothing is sent, and the allocator counters stay 0 because the library does not allocate from the title's pool (the console reports 6 allocations and 0x1260 bytes right after `sceRudpInit`, and 4 more allocations and 0x3F0 bytes per context). The console also writes `currentContexts` and the allocator counters when the byte count is smaller than their offset; only the requested bytes are written here. - [sceRudpGetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - measured on a PS5 (firmware 13.60) in #3073: before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED` and leaves the value untouched, also for a null pointer; after it a null pointer returns `RUDP_ERROR_INVALID_ARGUMENT` and a valid one gets 1410 with result 0. The library keeps no configurable segment size (there is no `sceRudpSetMaxSegmentSize`), so 1410 is always reported. - [sceRudpGetOption, sceRudpSetOption](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - full PS5 option contracts are unverified; defaults and validation follow [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/c86ac732bc811057e690bed280c5076bdf95385e/vita3k/modules/SceRudp/SceLibRudp.cpp). Options 12 and 13 have unknown meanings; last-error option 14 is not maintained. Their setters store all 32 bits per context and return 0 for valid four-byte buffers, preserving acceptance on `main`; their getters throw. Static inspection of PPSA06888 v01.001.008's bundled Unity transport shows its context-set constructor configuring IDs 2, 3, 5 and 6; runtime usage and other paths remain unverified. The plugin wrappers pass four-byte values. No context can connect, so stored settings cannot affect traffic. Closes when: PS5 option contracts are verified and the remaining behavior is implemented. -- [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - unverified on a PS5; imported by PPSA31327 and not called before the main menu. Signatures and the 8-byte event layout are recovered from the supplied module. The operations (1 add, 2 modify, 3 remove) and the argument checks follow the PS Vita library in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp), and the error codes match [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.h). Vita3K leaves `sceRudpPollDestroy` unimplemented, so `int sceRudpPollDestroy(int pollId)` and its checks are inferred. No context can connect, so no event is ever reported: `sceRudpPollWait` returns 0 when its timeout elapses, also for an empty poll, where Vita3K returns at once -- [sceRudpAccept, sceRudpFlush, sceRudpGetLocalInfo, sceRudpListen, sceRudpPollCancel and sceRudpSetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRudp.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. +- [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) - console hardware behavior and removal of the last entry during an existing wait remain unverified. A [firmware 13.42 report](https://github.com/boykopovar/AnyPS5/pull/10954#issuecomment-6398457761) executes the actual module under emulation with kernel/libc imports stubbed and condition waits scripted, not on console hardware: an empty poll waits for its remaining timeout, including after spurious wakes, and leaves event output untouched. This agrees with the retained wait behavior here and conflicts with the immediate empty return in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/c86ac732bc811057e690bed280c5076bdf95385e/vita3k/modules/SceRudp/SceLibRudp.cpp). The argument checks, control operations and 8-byte event layout retain their existing sources; the firmware report has not been independently reproduced here. +- [sceRudpPollCancel, sceRudpPollDestroy and sceRudpEnd](../../../core/libs/prx/libSceRudp/Export.cpp) - cancellation remains incomplete. PollCancel still throws through its parameterless placeholder; [Orbital b321fcd](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json) declares `int sceRudpPollCancel(int pollId)`. The [detailed firmware 13.42 report](https://github.com/boykopovar/AnyPS5/pull/10954#issuecomment-6399054346), under the emulated/stubbed imports described above, observes a signed low-32-bit argument and initialization, negative-ID and unknown-ID errors `0x80770001`, `0x80770004` and `0x8077001F`. Two idle cancels coalesce into one per-poll cancellation consumed even by a zero-timeout wait. In the scripted two-waiter Cancel/Destroy cases both waiters return `0x80770014`; those calls drain the waiters before returning, and Destroy then destroys the condition variable. A later wait on that destroyed ID returns `0x8077001F`. End similarly drains its active waiter with `0x80770014`; subsequent Wait/Cancel calls return `0x80770001`. Here Destroy/End return without draining and wake existing waiters with `0x8077001F`/`0x80770001`. Real-kernel wake/timeout races, simultaneous control calls, End/re-init overlap and extreme timeout boundaries remain untested; the report is not a console hardware measurement or an independent reproduction here. +- [sceRudpAccept, sceRudpFlush, sceRudpGetLocalInfo, sceRudpListen and sceRudpSetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) - remain throwing placeholders. Their names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRudp.c); their parameterless declarations solely resolve known imports and do not establish the ABI. 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/libSceSpSysCallWrapper.md b/docs/dev/debt/libSceSpSysCallWrapper.md deleted file mode 100644 index 1a664e18fd..0000000000 --- a/docs/dev/debt/libSceSpSysCallWrapper.md +++ /dev/null @@ -1,5 +0,0 @@ -# libSceSpSysCallWrapper technical debt - -### Unknown function info - -- [sceSpPthreadMutexattrSetprioceiling](../../../core/libs/prx/libSceSpSysCallWrapper/Export.cpp) (libSceSpSysCallWrapper) - still throws. In firmware 13.60's `libSceSpSysCallWrapper.sprx` it only jumps to libkernel's `scePthreadMutexattrSetprioceiling`, which AnyPS5 does not implement yet (read from disassembly, not measured on a console) 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/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/libSceVideodec2.md b/docs/dev/debt/libSceVideodec2.md index 74a8f81c0f..6cea4cb6a6 100644 --- a/docs/dev/debt/libSceVideodec2.md +++ b/docs/dev/debt/libSceVideodec2.md @@ -11,9 +11,10 @@ - [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. - [sceVideodec2Decode](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) and `sceVideodec2Flush` output the coded picture, with the chroma plane after its luma rows, as the console does for 1080p H.264 and HEVC streams coded at 1920x1088. An HEVC or VP9 picture whose size is not a multiple of 16, such as HEVC coded at 1920x1080, is output at that size; whether the console pads it is unverified. `sceVideodec2GetPictureInfo` reports no frame cropping - [sceVideodec2Decode of an empty access unit](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) returns `0x811D010E` for a null `auData` and `0x811D010D` for an `auSize` of 0, with no picture, where it threw before. The values are `ACCESS_UNIT_POINTER` and `ACCESS_UNIT_SIZE` in the error table of [shadPS4's videodec_error.h](https://github.com/shadps4-emu/shadPS4/blob/f3f0c14c1350a768558dfd1fd058d259ccecaca2/src/core/libraries/videodec/videodec_error.h), whose libSceVdecsw codes this repository already uses; they are not measured on a PS5, and neither is whether the console leaves the decoder's state alone on that call, as this does. Media Player Classic PS5 (PPSA99015) submits empty units: its own converter reads a 4-byte length prefix of `00 00 01 xx` as a start code and drops the rest as a non-base HEVC layer. It goes on after the error. +- [sceVideodec2Decode and sceVideodec2Flush](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2): the decoder core's discarded flag is not modelled: a returned picture always has `OutputInfo.isDiscardedFrame` (byte 0xb) at 0, and an unshown VP9 frame returns no picture. Per a #10913 review reading firmware 13.42's module (not reproduced here), the byte is written only with a returned picture and is left as the caller set it otherwise, as here. Name and offset: [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs#L150-L192) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/83f8bfd3c9c35fcb135ef6c33dfd88bbfe066ee2/src/libs/libVideoDec2.cpp#L91-L105); shadPS4 leaves it as padding. PPSA05110 sets only `thisSize` before its VP9 Decode (0x13728dd) and Flush (0x1371f7e) and takes a picture only when the byte is 0 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/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 e7885de76f..3c90a5c32c 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -1,5 +1,9 @@ # libc technical debt +### Silent stubs + +- [_Mtx_current_owns](../../../core/libs/prx/libc/src/Threads.cpp) (libSceLibcInternal, implemented in libc, NID `VYQwFs4CC4Y`) returns 1 and ignores its argument. This follows the PS5 12.00 body reading reported in [PR8859](https://github.com/boykopovar/AnyPS5/pull/8859), rather than inspecting host mutex ownership. The firmware observation is attributed to that contributor; other firmware versions and the argument's console type remain unverified. + ### Unknown function info - [strtonum](../../../core/libs/prx/libc/src/StringExtensions.cpp) (libc) is unverified on the console. Its signature, decimal parsing, inclusive bounds, error strings, error precedence and errno preservation follow [FreeBSD 11.4 strtonum.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/strtonum.c). Parsing uses the host `strtoll` with base 10. It is not exported by `libSceLibcInternal.sprx` on a PS5 (firmware 13.60), so a payload process cannot call it there @@ -7,7 +11,7 @@ - [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. 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. @@ -17,11 +21,11 @@ - [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. +- [fegetround, fesetround, fetestexcept, feclearexcept, fegetexceptflag and fesetexceptflag](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) follow a payload process on a PS5 (firmware 13.60): the rounding modes are x87 control bits 10-11 (0, 0x400, 0x800, 0xc00), `fesetround` sets them and the MXCSR rounding bits and returns -1 for any other value, `fegetround` reads the x87 bits, and the exception flags are 1, 4, 8, 0x10 and 0x20 with a 2-byte `fexcept_t`. `fetestexcept` returns the x87 status word and MXCSR together, masked only by its argument, so a mask past 0x3f also returns their other bits. They access the current host thread's x87 and MXCSR state, which starts with flush-to-zero and denormals-are-zero set on the console. 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. Null flag pointers throw instead of faulting. - [feenableexcept, fedisableexcept, __feenableexcept, __fedisableexcept, fegettrapenable and fesettrapenable](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libc) change only the x87 masks under Rosetta 2: MXCSR keeps its six exception masks set there, so no SSE trap is enabled, and the enable and disable results, which report the traps enabled on both units, report none - [fegetenv](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `psx0YiAKm7k`) follows FreeBSD 11 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/amd64/fenv.c) with the 32-byte `fenv_t` of [x86/fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): `fnstenv` fills the 28-byte x87 part, MXCSR follows at 0x1c, and the x87 control word is reloaded because `fnstenv` masks every x87 exception. That the console's `libSceLibcInternal` has the same code and layout is assumed, not read from its binary. A null pointer throws instead of faulting - [feholdexcept](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `1kduKXMqx7k`) - console equivalence and module/library versions are unverified. The signature and 32-byte amd64 environment layout follow the public SDK [fenv.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/fenv.h); saving both units, masking exceptions and clearing pending flags follows FreeBSD 11 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/amd64/fenv.c). The NID comes from the public 3.20 stub inventory, not a module examined here. Null storage throws before modifying the environment, where the reference dereferences it -- [feupdateenv](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `YXQ4gXamCrY`) follows FreeBSD 11.4 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/msun/amd64/fenv.c): capture the old x87/MXCSR exception union (`0x3f`), restore the saved 32-byte environment, raise those flags in both units, then `fwait`. The console's layout, behavior and module versions are unmeasured; state belongs to the current host thread. A null input or MXCSR bits unsupported by the host's `FXSAVE` mask throw before changing floating-point state; the mask's architectural zero fallback is `0xffbf`. Console fault behavior for malformed environments is unknown. The separate `__fe_dfl_env` object remains unimplemented; no host `FE_DFL_ENV` sentinel is substituted. +- [feupdateenv, __fe_dfl_env](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `YXQ4gXamCrY`) - a null environment or MXCSR bits the host's `FXSAVE` mask does not support (zero mask: `0xffbf`) throw before changing state, where the system `libSceLibcInternal` of PS5 firmware 13.60 faults (`SIGSEGV`, and `SIGBUS` for MXCSR `0xffff9fc0`). Otherwise it matches that library: it restores the 32-byte environment and raises the union of the x87 and MXCSR flags pending before the call in both units, as FreeBSD 11.4 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/msun/amd64/fenv.c) does, and an unmasked pending flag traps. `__fe_dfl_env` holds the 32 bytes that library exports: x87 control `0xffff037f`, status `0xffff0000`, tag `0xffffffff`, `0xffff` at offset 26 and MXCSR `0x9fc0`. State belongs to the current host thread - [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws. The console's low-address layout is not measured. Linux and Windows reserve free addresses from 64 KiB, except host mappings and low chunks the Linux address policy refuses; null hints still allocate from 8 GiB. macOS and ASan builds still start the arena at 8 GiB and cannot supply low 32-bit addresses. Guest thread creation ignores a stack address set in its attributes. A host-loaded executable is not moved, so a PIE above the arena gets no memory within 2 GiB of its code ([#5217](https://github.com/boykopovar/AnyPS5/issues/5217)) - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error - [sceLibcInternalHeapErrorReportForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(void* heap, void* block, uint32 error)` writes to the guest's standard output what a payload process on a PS5 (firmware 13.60) wrote: `[SceLibc] A heap error is detected.`, `[SceLibc] -- Mspace Name --`, then the C string at heap+0x3B0 followed by ` (%2u)` of the 32-bit value at heap+0x3F4 when it is not 0, the reason `segment out of range.` for error 1 only (38 other measured values print none), and `pid:<0x..> tid:<0x..>`, and returns. The pid and tid are the host's, where the console prints its own process and kernel thread ids. The block's contents did not change the console's output and it is not read here; the values and the guest return address also go to the host's standard error. A null heap faulted on the console and throws here. What the other heap fields and error values mean is unknown @@ -34,13 +38,15 @@ - [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. +- [feraiseexcept](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc, NID `NIfFNcyeCTo`) - console behavior and module/library versions have not been measured. The signature follows the [public PS5 SDK](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/fenv.h); flag updates and x87 trap delivery follow [FreeBSD amd64 fenv.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/msun/amd64/fenv.c). All six defined exception bits are supported. Unlike the FreeBSD reference, which does not mask or validate the requested bits, this implementation rejects other bits with `std::invalid_argument` before modifying state; console behavior for those bits is unknown. This is distinct from `_Feraise`, which also modifies errno and adds inexact for range exceptions. - [_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. +- [random, srandom, initstate, setstate](../../../core/libs/prx/libc/src/Random.cpp) (libc) - a null state throws where the system `libSceLibcInternal` of PS5 firmware 13.60 faults, `initstate` below 8 bytes skips that library's "not enough state" warning, and concurrent callers (one lock here) and a title's own libc are not measured. Otherwise it follows payload processes on PS5 firmware 12.70 and 13.60: a fresh process draws the `srandom(1)` sequence; `srandom` seeds through `good_rand` (int32 argument, 0 becomes 123459876, `16807 * (x % 127773) - 2836 * (x / 127773)` plus 0x7fffffff when negative); `initstate` picks types 0 to 4 by size, returns null below 8 bytes without touching `errno` but still saves the current rear position; both calls return the previous state; `setstate` takes a rear position past the degree unchecked - [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. - [Integer compiler-rt builtins](../../../core/libs/prx/libc/src/IntegerBuiltins.cpp) (libc) - the console traps in `__divsi3`, `__modsi3` and the 64- and 128-bit divisions on a zero divisor (`div`) and in `__negv*`, `__absv*` and `__mulv*` on overflow (an abort helper, then `ud2`); these throw instead. As in the `libc.prx` bundled with PPSA08427 (disassembly), `__addv*` and `__subv*` wrap without an overflow check, and `__udivsi3`, `__umodsi3` and `__udivmodsi4` divide in software, giving a quotient of 0 and the dividend as remainder for a zero divisor; `__clzdi2` and `__ctzdi2` of 0 return 32 and `__clzti2` and `__ctzti2` of 0 return 64 - [qsort](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) follows FreeBSD 11 [qsort.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libc/stdlib/qsort.c): Bentley and McIlroy's quicksort, with insertion sort below 7 elements and after a partition pass that swaps nothing, so elements that compare equal end in FreeBSD's order and the comparator is called in the same sequence. That the console's libc uses this FreeBSD code is unverified @@ -48,8 +54,8 @@ - [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) / 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](../../../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. +- [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). 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`. @@ -65,16 +71,19 @@ - [snprintf and vsnprintf](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) accept any size up to `SIZE_MAX`, as a payload process on a PS5 (firmware 13.60) does. A null buffer with a nonzero size throws here, where the console writes through it, and a result longer than `INT_MAX` throws, which was not measured +- [fpurge](../../../core/libs/prx/libc/src/StreamPurge.cpp) follows FreeBSD 11.4 [fpurge.c](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/stdio/fpurge.c): discard buffered input, pushback and output without writing or seeking, preserving EOF and error status, and report `EBADF` for a closed stream. Console behavior is unverified. The Windows path resets the existing host UCRT buffer representation; Linux and macOS use their native purge operations. Null streams throw as existing stream wrappers do. + ### Functional - [_Cosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the two-argument Dinkumware CRT helper used by `std::complex`, including a scaled large-finite-input path; the signature, exact result and floating-point exception behavior are not verified against the console. - [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. +- [_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. @@ -112,10 +121,13 @@ - [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 @@ -136,16 +148,18 @@ - 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 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. Infinities and `long double` 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 still formats through the host. +- [%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. +- [codecvt, numpunct, num_put and time_put _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build facets matching a bundled console `libc.prx` (dynsym decoded by NID, vtables disassembled): categories 2, 4, 4 and 5, sizes 0x10, 0x30, 0x10 and 0xb8. Unlike `collate`, they ignore the locale argument and always build the C-locale facet; the console's behaviour for a non-C name is unverified. `time_put`'s `do_put` formats through this library's `strftime` instead of the facet's unpopulated `_Locinfo` table, so the `strftime` gaps apply. `codecvt` reproduces the console virtuals (`noconv` conversions, `length` the shorter range); `numpunct` copies its C-locale table (`""`, `.`, `,`, `false`, `true`). The facets share the no-op `destroy`/`deleteObject`/`retain`/`release` base used for `collate`, so `numpunct`'s static C-locale strings are never freed. +- [num_put::do_put](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) follows the SDK 7.00 `libc.prx` that PPSA06416 ships, read from its disassembly and run, but formats through this library's `snprintf`. That converts floating point exactly and rounds ties to even, where the module keeps about 18 exact significant digits, prints later ones from an inexact scaling (1e10 at precision 22 is `9999999999.999999999999`) and gives `-3` for -2.5 at precision 0. A precision so negative that the module's buffer of precision + 50 bytes cannot hold the result throws, where the module reads stale bytes. A stream locale without a `ctype` facet leaves digits unwidened, as the console's C-locale facet does. +- [std::clog, std::wclog and the facet ids](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc): `std::clog` and `std::wclog` are 0x400-byte zeroed objects like the rest of the `NSTDSTR` stream set (the console's `libc.prx` sizes `std::clog` at 96 bytes, measured from its dynsym `st_size`), and the `numpunct::id` and `time_put::id` objects are 8-byte zeros like the other facet ids. - [std::__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`. - [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 @@ -156,26 +170,33 @@ - [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount - [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. A payload process on a PS5 (firmware 13.60) read both names empty with `TZ` unset, and its `%Z` printed nothing whatever `tm_zone` held; the names a title process sees are not known - [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. On macOS `mktime` converts through the host's `mktime`, which refuses every year before 1900. A local time before 1900 is instead solved for directly: the offset `localtime_r` gives for a candidate time, which follows the zone's history (local mean time before its first transition), is subtracted from the local fields, and the step is repeated until the offset holds. `tm_isdst` is not consulted there, so a time with `tm_isdst` set where the zone had no daylight offset is taken as standard time. FreeBSD's [`time1`](https://github.com/freebsd/freebsd-src/blob/stable/11/contrib/tzcode/stdtime/localtime.c#L2036) would instead shift it by the difference between one of the zone's daylight types and a standard one. `gmtime` converts every time whose year fits an `int` on both hosts. -- [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` +- [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow a payload process on a PS5 (firmware 13.60) in the C locale. Out-of-range fields in `%G`, `%g`, `%V`, `%U` and `%W` follow the SDK 7.00 `libc.prx` that PPSA06416 ships, read from its disassembly and run, and are measured on a console only for the cases in the test; it counts a year divisible by 100 as leap only when `tm_year / 400` is 100 or -300. `%k`, `%l`, `%P`, `%s`, `%+`, `%v`, flags and widths are copied as written, `E` or `O` before an unknown conversion is dropped, and a format ending in either throws, where the console crashed. Negative fields print 0 and fields wrap at 100 (`%j` at 1000, `%u` and `%w` at 10); `%Y` and `%G` are the year mod 10000, `%C` is `tm_year / 100 + 19`. A negative weekday or month gives `Sun` or `Jan`. `%z` and `%Z` ignore `tm_gmtoff` and `tm_zone` and use the guest zone and `tzname` - [wcsftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) 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)), 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. +- [collate vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `ruZtIwbCFjk`) is nine `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). Slots 2 through 8 hold the same `DestroyCollate`, `DeleteCollate`, `RetainFacet`, `ReleaseFacet`, `CollateCompare`, `CollateTransform` and `CollateHash` functions that `g_collateVtable` uses, so facet calls through the exported object work; offset-to-top and typeinfo remain zero, unlike the console's relocated entries, and `_Getcat` still installs `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 177c833bf4..849d260421 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -2,37 +2,44 @@ ### Silent stubs -- [SO_TIMESTAMP, SO_BINTIME](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - set and read back as their bits, as a payload process on a PS5 (firmware 13.60) answered them, but `recvmsg` adds no `SCM_TIMESTAMP` or `SCM_BINTIME` control message, which FreeBSD 11.4's `ip_savecontrol` and `ip6_savecontrol` add to a received datagram while the option is set; what the console's `recvmsg` returns was not measured +- [SO_TIMESTAMP, SO_BINTIME](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the arrival time in their control messages is `CLOCK_REALTIME` read when the host hands the datagram over, not when it reached the socket. Otherwise `recvmsg` on a UDP socket returns them as a payload process on PS5 firmware 13.60 got them: one 32-byte `SCM_BINTIME` (IPv4 only) then one `SCM_TIMESTAMP` at `SOL_SOCKET`, and a control buffer too small for them gets as many bytes as fit, its length, and `MSG_CTRUNC`. That partial copy is the console's: FreeBSD 11.4's `kern_recvit` copies whole messages only. Not measured, after 11.4: an empty buffer gets nothing and `MSG_CTRUNC`, no buffer gets nothing, and a TCP or Unix socket gets no message - [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. -- [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. +- [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. Measured on 13.60: the same holds for values 1, 0x1000 and 2^64-1, this API still reports "13.590.001" while the Prospero variant reports "13.600.007", and a null pointer returns 0 without a fault or an `errno` change. Other firmware versions 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` +- [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; the console's value is unverified and no `times` reports ticks at that rate. CPython in PS5MC (PPSA99420) requires it >= 1. `_SC_ARG_MAX` returns 262144, `_SC_CHILD_MAX` the `RLIMIT_NPROC` limit via `getrlimit`, `_SC_NGROUPS_MAX` 1023 (`KERN_NGROUPS`), `_SC_OPEN_MAX` the `RLIMIT_NOFILE` limit via `getrlimit`, `_SC_STREAM_MAX` that limit bounded by `SHRT_MAX`, `_SC_HOST_NAME_MAX` 255, `_SC_LOGIN_NAME_MAX` 33, and `_SC_IOV_MAX` 1024 (`UIO_MAXIOV`). Other names 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 +- [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). Measured on PS5 firmware 13.60: every size (0, 0x27, 0x28, 0x29, 0x30, 0x100 and 2^64-1) returns 0 and is preserved, bytes 8 to 0x27 are written and nothing from 0x28 onward, and a null pointer returns 0 without a fault or an `errno` change - [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` @@ -62,45 +69,45 @@ - [Protecting a host thread's stack](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - on Windows `sceKernelIsStack` reports a host thread's whole stack reservation, which Windows commits on demand from the top down behind a guard page. A protect over that range changes only the pages above the guard; the guard page and the reserved pages below it are left as they are, so the stack still grows and an overflow still raises a stack overflow. A protect that starts at the bottom of the reservation and stops short of the guard page, as HotSpot makes its stack guard zones, is committed and protected. Windows moves the guard page down by committing the pages below it as read/write guard pages, whether they were reserved or committed, so those pages come in read/write as the stack grows to within one guard region of them (three pages on GitHub's runners). A protection other than read/write on them, `PROT_NONE` included, does not hold once the stack is that close, where on the console every page of the reported range takes the protection. Holding it would mean removing the thread's guard page and keeping the stack limit in its TEB by hand. On Linux the reported range of the main thread is mostly unmapped; the unmapped pages from the bottom of a protect up to the top of the stack are mapped read/write in place, then the protect is applied, so the stack never has to grow down to the protected pages, which Linux refuses within its stack guard gap (1 MiB by default) above a readable mapping. The pages below a protect stay unmapped and out of the stack's reach, where on the console the stack goes on below them - [Foreign guest module image protection on Windows](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMprotect` on Windows allows protection mutations on mapped PE images containing a `.elf0` section (as relinked guest modules produce, such as NVIDIA Real-time Denoisers `libNRD.prx` bundled in Dying Light 2: Stay Human PPSA02262), registering their sections into `GuestAllocations`. Host libraries and system DLLs (such as `libkernel.prx` and `kernel32.dll`) without a `.elf0` section continue to be rejected - [_umtx_op](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the mutex, condition variable, read-write lock, legacy lock and `SEM2` operations throw, and so does a misaligned object. Implemented as FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE`, `UMTX_OP_NWAKE_PRIVATE`, `UMTX_OP_SEM_WAIT` and `UMTX_OP_SEM_WAKE` (the last two built under `COMPAT_FREEBSD9`/`10`). Operations 0 and 1 fail with `EOPNOTSUPP`, as measured on a PS5 (firmware 12.70). The `SEM2` operations, 23 and 24, were measured on the console and returned `0` for a count of 0 where FreeBSD would time out, so they stay unimplemented. -- [sceKernelCreateSema, sceKernelPollSema, sceKernelSignalSema, sceKernelDeleteSema](../../../core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp) (libkernel) - a poll, a signal or a second delete of a deleted semaphore reads freed memory here, where the console returns `0x80020003` (ESRCH) ([measured](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/kernel.golden.tsv#L17-L19)). A null output pointer to `sceKernelCreateSema`, an attribute above 2, a negative maximum and a negative signal count throw, as they were not measured. `SCE_KERNEL_ERROR_EINVAL` for an initial count above the maximum or negative, a maximum of 0, a null name, a poll of 0 or above the maximum and a signal of 0 is [measured](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/kernel.golden.tsv#L10-L23). +- [sceKernelCreateSema, sceKernelPollSema, sceKernelSignalSema, sceKernelDeleteSema](../../../core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp) (libkernel) - a poll, a signal or a second delete of a deleted semaphore reads freed memory here, where the console returns `0x80020003` (ESRCH) ([measured](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/kernel.golden.tsv#L17-L19)). A null output pointer to `sceKernelCreateSema` (a fault on PS5 firmware 13.60) and attributes 0x100 to 0x102 (accepted there, effect unknown) throw. Other attributes above 2 and negative maximum and signal counts return `EINVAL` here, as there. `EINVAL` for an initial count above the maximum or negative, a maximum of 0, a null name, a poll of 0 or above the maximum and a signal of 0 is [measured](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/kernel.golden.tsv#L10-L23). - [sceKernelSyncOnAddressWait](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded - [scePthreadMutexTimedlock](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp), [scePthreadRwlockTimedrdlock, scePthreadRwlockTimedwrlock](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - a timed lock of a mutex the caller holds, or of a rwlock it holds for writing, sleeps until its timeout before returning `ETIMEDOUT` (`0x8002003c`), as FreeBSD 11.4 libthr does ([mutex](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L878-L928)); whether the console waits is not measured. The prosper oracle measured only the result, for a default mutex and a rwlock with a 1000 us timeout ([kernelx.golden.tsv](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/kernelx.golden.tsv#L16-L47)); the other mutex types and the POSIX timed locks follow the same rule unmeasured. -- [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - 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 +- [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 -- [pthread_main_np](../../../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; 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) - 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 @@ -120,12 +127,13 @@ - [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 +- [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 - [pthread_timedjoin_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel, NID `PkS44IGrDkM`) - the signature follows [ps5-payload-dev/sdk](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/pthread_np.h#L71); deadline validation, absolute realtime deadlines, error precedence and join ownership follow [FreeBSD libthr](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libthr/thread/thr_join.c). The [public 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libkernel.c) lists this NID, but PS5 behavior and module/library versions have not been measured. Realtime is rechecked at most two seconds apart while waiting. After guest cleanup completes, final native thread teardown uses the existing unbounded join and may finish after the deadline; host teardown is not bounded by this implementation. +- [sceKernelMapNamedSystemFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the PS5 system budget and the `sceKernelVirtualQuery` record of a system map are unmeasured. It is not charged to the game flexible budget, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/30f4170896ce974353a6f6486d0ecec179aec54b/src/core/libraries/kernel/memory.cpp#L354-L391), which charges only its flexible type; fpPS4 ignores `MAP_SYSTEM` and charges it. It reports as flexible, as in fpPS4; shadPS4 reports it as not flexible. Checks, alignment and placement are `sceKernelMapNamedFlexibleMemory`'s, so flag 0x8000 and protection 0x800 throw, unmeasured. A null name or one of 32 bytes or more throws, unmeasured: shadPS4 returns `EFAULT` or `ENAMETOOLONG` before mapping, [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_map_mm.pas#L1121-L1209) maps it unnamed. Closes when: measured on a PS5 ### Functional @@ -142,13 +150,13 @@ - [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 - [EVFILT_VNODE on macOS](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) registers `EVFILT_VNODE` on a duplicate of the descriptor in a host kqueue, whose notes have FreeBSD's values, and drops the `NOTE_LINK` that XNU adds to `NOTE_DELETE` when the watched file or directory is removed, since FreeBSD's `vop_remove_post` and `vop_rmdir_post` report `NOTE_DELETE` alone. As on Windows, a directory registered with `NOTE_EXTEND` is accepted but never receives it: XNU reports a move between directories as `NOTE_WRITE` alone, with `NOTE_LINK` for a subdirectory - [Windows file identity](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails -- [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction is reported the same way when the name left after that prefix is a drive path (`X:\...`): the print name when that name is a drive path, otherwise the substitute name. `open` with `O_NOFOLLOW` returns `EMLINK` for that junction and does not open the target +- [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction is treated as a link only when `ReadJunction` in [WindowsSymbolicLink.hpp](../../../core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp) accepts a drive-path target (`X:\...`) after stripping `\??\` / `\\?\`; volume mounts and other non-drive substitutes are not. Consumers of that drive-target path: [NativeStat.cpp](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (`lstat`/`readlink`), [Open.cpp](../../../core/libs/prx/libkernel/File/src/Open.cpp) (`O_NOFOLLOW` → `EMLINK`, and `unlink` removing the junction), [Stdio.cpp](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (`rmdir` → `ENOTDIR`), and [Export.cpp](../../../core/libs/prx/libkernel_sys/Export.cpp) (`unlinkat` with and without `AT_REMOVEDIR`). `unlink` / plain `unlinkat` leave the target, including when it is not empty - [lstat of a Windows reparse point that is not a link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a mounted volume (`\??\Volume{GUID}\`) or another name surrogate as the reparse point itself, not followed, with the mode of its attributes, as CPython's `os.lstat` does, and a reparse point without the name surrogate bit (deduplication, cloud files) as `stat` reports it - [setitimer](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `sys_setitimer` and `kern_setitimer`: `EINVAL` for timers other than `ITIMER_REAL`, `ITIMER_VIRTUAL` and `ITIMER_PROF`, and for a value, or the interval of a non-zero value, with a negative field, `tv_usec` of 1000000 or more, or more than `INT32_MAX / 2` seconds; a null value reads the timer as `getitimer` does, with `EFAULT` when the output is null too. `ITIMER_REAL` shares the timer thread of `alarm`, which clears its interval: a zero value disarms it, the previous value is returned, and each expiry re-arms it by `it_interval`. With `SIG_IGN` an expiry is dropped. With `SIGALRM` blocked it is left on the pending set of `sigtimedwait`, as `kern_psignal` does, where `sigwait`, `sigtimedwait` and `sigwaitinfo` take it; unblocking it runs a guest handler on the unblocking thread, and with the default disposition exits the process with status 142. Unblocked, the default disposition exits the process with status 142 and a handler throws, since guest `SIGALRM` handlers are never run from the timer thread. The value is not raised to `tick / 16` as `itimerfix` does, and an interval below 1 ms throws, since the console's `hz`, and so that rounding, is unknown (1 ms covers any `hz` of 63 or more). `ITIMER_VIRTUAL` and `ITIMER_PROF` throw when armed, as process CPU time is not accounted, and read and disarm as zero. PPSA99420 imports it and fails to load without it - On Linux, [direct memory](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. @@ -156,8 +164,9 @@ - [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, 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. @@ -172,7 +181,7 @@ - [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist, except `/gkbHtXc3Wq/common/cert/CA_LIST.cer`, which [SandboxCertificates.cpp](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) serves from the host trust store - [ktimer_create, ktimer_settime, ktimer_gettime, ktimer_getoverrun, ktimer_delete](../../../core/libs/prx/libkernel/Time/Ktimer.cpp) (libkernel) - a `SIGEV_SIGNAL` or `SIGEV_THREAD_ID` timer, and one created with a null `sigevent` (FreeBSD defaults it to `SIGEV_SIGNAL`), throws, because delivering a guest signal from a timer is not modelled. A non-zero value under one tick rounds up to 1 ms, as FreeBSD 11 does at hz 1000; the console's hz is not measured (`kern.clockrate` is unanswered). `SIGEV_NONE` timers on `CLOCK_REALTIME` and `CLOCK_MONOTONIC` follow `kern_time.c`: ids 3 to 31 (`EAGAIN` when full), `EINVAL` for another clock, a bad notify type or signal, an id that is not live or a bad `itimerspec` field, and `TIMER_ABSTIME` (1). Expiry is worked out from the guest clock when the timer is read, and `ktimer_getoverrun` returns 0, as FreeBSD only updates it when a signal is accepted - [The sandbox CA list](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) (libkernel), `//common/cert/CA_LIST.cer`, is a PEM bundle of the host's trust anchors, written once per process to `cert_cache/CA_LIST.cer` in the run directory with the owner write bit cleared: on Windows the current user's `ROOT` store, filtered through Disallowed and the combined EKU extension/property for TLS server authentication (Windows fetches missing roots on demand, so a root that was never fetched is absent), elsewhere `SSL_CERT_FILE` or the first of `/etc/ssl/certs/ca-certificates.crt`, `/etc/pki/tls/certs/ca-bundle.crt`, `/etc/ssl/ca-bundle.pem` and `/etc/ssl/cert.pem` that exists. On macOS, `/etc/ssl/cert.pem` is a static snapshot and does not reflect roots added to or distrusted in the Keychain. Existing guest files, explicit aliases and blocked mounts take precedence over the provider. The PEM snapshot does not carry Windows CTL restrictions beyond EKU, native revocation policy or later trust-store changes. Without any certificate the path does not exist. The console file's format is unverified: only that bundled OpenSSL accepts a PEM bundle there is shown (ProsperoRadio PPSA99001 and SwanStation PPSA98510, #5001). The other entries of the console's `common/cert` directory, the directory itself (it does not exist here) and the error a write open gets (`EACCES` from the host here) are unverified -- [pselect](../../../core/libs/prx/libkernel/Socket/src/Pselect.cpp) (libkernel) sets the signal mask with `sigprocmask`, calls `select` and restores the mask, as FreeBSD 11 `kern_pselect` does, but the swap is not atomic with the wait: a signal that becomes deliverable between the swap and the wait is not guaranteed to interrupt it +- [pselect](../../../core/libs/prx/libkernel/Socket/src/Pselect.cpp) (libkernel) sets the signal mask, calls `select` and restores the mask, as FreeBSD 11 `kern_pselect` does. A pending signal that the new mask unblocks has its handler run during the swap, and `pselect` then returns `EINTR` unless a descriptor is already ready, the timeout is zero or an argument is invalid, as FreeBSD's `kern_select` decides before it sleeps. A signal raised during the wait, including one sent by another thread, does not interrupt it, because `select` has no `EINTR` path; and the mask is the process-wide one, so it applies to every thread for the duration of the call - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them - [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only @@ -190,31 +199,30 @@ - [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 -- [scePthreadCreate and scePthreadDetach, measured](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - as in a payload process on a PS5 (firmware 13.60): a name of 32 bytes or more returns `ENAMETOOLONG` (`0x8002003f`) without creating a thread, 31 is kept whole, and a null name gives an empty one; detaching a joined thread returns `ESRCH`; detaching a thread another thread is joining, or a detached one, returns `EINVAL`, as does joining a detached one; a second concurrent join returns `EOPNOTSUPP`; a null thread returns `EINVAL` from both; a null handle pointer to `scePthreadCreate` faulted and throws here. Whether the handle is written before the name check was not measured; here it is left alone +- [scePthreadCreate and scePthreadDetach, measured](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - as in a payload process on a PS5 (firmware 13.60): a name of 32 bytes or more returns `ENAMETOOLONG` (`0x8002003f`) without creating a thread and sets the handle to 0 (32 and 33 tried), 31 is kept whole, and a null name gives an empty one; detaching a joined thread returns `ESRCH`; detaching a thread another thread is joining, or a detached one, returns `EINVAL`, as does joining a detached one; a second concurrent join returns `EOPNOTSUPP`; a null thread returns `EINVAL` from both; a null handle pointer to `scePthreadCreate` faulted and throws here; `pthread_create_name_np` goes through `scePthreadCreate` and so also clears the handle on a long name, but only the sce form was measured - [Thread cancellation](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) follows FreeBSD libthr (states and types 0/1 and 0/2, `PTHREAD_CANCELED` 1, the `_pthread_cleanup_info` node layout), not measured on the console. A request is acted on only in `scePthreadTestcancel`, the condition variable, `sem` and `scePthreadSem` waits, `scePthreadJoin` and the sleeps, and their POSIX names; file and socket calls are not cancellation points. An enabled cancellation request wakes POSIX `sleep` and exits the thread. `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check cancellation only before and after the host sleep, so an enabled request during those waits is delivered when the sleep ends. The asynchronous type acts like the deferred one, except at the two set calls. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [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 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](../../../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. +- [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.pid](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - what the console fills in a process's `kinfo_proc` is unverified; `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_PID`, pid} for the calling process returns a 1,088-byte FreeBSD 11 amd64 `kinfo_proc` ([`sys/sys/user.h`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/sys/user.h)) holding only `ki_structsize`, `ki_pid`, `ki_ppid` and `ki_comm` (`eboot.bin`, the base name of the `kern.proc.pathname` value), with every other field 0, including `ki_flag`, so `P_TRACED` (0x800) is clear. Source Port (PPSA20040) asks for its own record with a 0x440-byte buffer and tests only bit 0x800 of `ki_flag` at offset 0x170, to see whether a debugger is attached. Any other pid, including -1, fails with `ESRCH`, a write with `EPERM` and a short buffer with `ENOMEM`, as for `kern.proc.vmmap`. - [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 +- [sysKernelGetUpdVersion, sysKernelGetManufacturingMode, sysKernelGetIntdevModeForInternalLibc, sysKernelGetIntdevModeForRcmgr](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - follow firmware 13.42 disassembly measured by BunnyFl, querying sysctlbyname: `sysKernelGetUpdVersion` checks null (`SCE_KERNEL_ERROR_EINVAL`) and queries `machdep.upd_version`; `sysKernelGetManufacturingMode` queries `machdep.curr_manumode`; `sysKernelGetIntdevModeForInternalLibc` and `sysKernelGetIntdevModeForRcmgr` query `machdep.rcmgr_intdev_for_internal_libc` and `machdep.rcmgr_intdev`. On AnyPS5, these sysctls return 0 as assumed defaults and reject writes with `EPERM`; their values and write policy on retail consoles are unmeasured - [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 - [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 -- [execvp](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created. - [nmount](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throws: mounting file systems is not supported. Prospero Themes (PPSA90210) imports it and fails to load without it; it opens its theme library without calling it, and its strings (`/dev/ssd0.system_ex`, `/system_ex`, `exfatfs`, `fstype`, `fspath`, `from`, `ignoreacl`) point to remounting the console's `/system_ex` partition to write theme files there. - [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and macOS and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows and macOS, which has no such `AF_UNIX` pair. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and 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. @@ -224,23 +232,24 @@ - [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 - [`shutdown(SHUT_RD)`](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) on a Windows TCP socket resets the connection when data is unread or arrives later, so the peer's next send fails with `EPIPE`, where FreeBSD's [`sorflush`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L2387-L2432) discards the data and the peer's sends succeed. On a Unix stream socket FreeBSD keeps appending the peer's later data ([`uipc_send`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_usrreq.c#L936-L1001) does not check the receiver's `SBS_CANTRCVMORE`) and receives return it; here receives return 0 and the peer's send fails with `EPIPE` on Linux. - [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 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, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - a connected stream socket's `sendto` ignores the address once its length passes, as FreeBSD 11.4's `tcp_usr_send` does, which was not measured. Otherwise they check a socket address as a payload process on a PS5 (firmware 13.60) did on datagram and stream sockets: a length below 2 fails with `EINVAL` and above 255 with `ENAMETOOLONG`; then an `AF_INET` socket needs exactly 16 bytes and an `AF_INET6` socket 28, or `EINVAL`, before the family is looked at (an `AF_INET6` address of 28 or 10 bytes on an `AF_INET` socket fails with `EINVAL`); then another family, `AF_UNSPEC` with `INADDR_ANY` included, fails with `EAFNOSUPPORT`; and `connect` on a listening stream socket fails with `EOPNOTSUPP` - [bind, connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) on an `AF_INET6` datagram socket with `IPV6_V6ONLY` cleared accept an IPv4-mapped address longer than 28 bytes and use its first 28, as FreeBSD 11.4's `udp6_bind` and `udp6_connect` hand it to `in_pcbbind` and `in_pcbconnect` as a `sockaddr_in` without the length test of `in6_pcbbind`; `sendto` still fails it with `EINVAL`, as `udp6_send` tests the length first. The console was measured only with `::1` and `IPV6_V6ONLY` at its default, so this case is not measured - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set, and the guest cannot ignore it: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor +- [write to a pipe without a reader](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) - `write`, `sceKernelWrite`, `writev` and `sceKernelWritev` fail with `EPIPE` once the read end is closed, as FreeBSD 11.4's [`pipe_write`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_pipe.c#L1062-L1069) does; on Windows the host reports `ERROR_NO_DATA`, which the C runtime would turn into `EINVAL`. [`dofilewrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L597-L601) also sends `SIGPIPE` to the writing thread: nothing is raised on Windows, and on Linux the host raises its own `SIGPIPE`, which the guest cannot ignore, as for sockets above. Not measured on a console - [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 and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number -- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A 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 +- [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end's st_ino comes from a counter this process starts at 1, where FreeBSD's uipc_sense uses one counter for every local socket on the system, so the numbers differ from the console's +- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A kqueue is ready for `POLLIN` and `POLLRDNORM` while one of its events is triggered and for no other event, as FreeBSD 11.4's [`kqueue_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c#L1900-L1924) answers. A kqueue cannot be waited on either, so on every host `poll` checks it every millisecond and waits on the other descriptors in slices of 1 ms. A kqueue closed during the wait reports `POLLNVAL`, and one whose number was reused is polled again with the rest of the timeout, as FreeBSD 11.4's [`pollrescan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1499-L1519) looks the descriptor up again. A close that has already removed the kqueue but not yet closed its host descriptor is seen as a descriptor of unknown kind, which throws. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. `/dev/random` and `/dev/urandom` follow FreeBSD 11.4's [`randomdev_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/dev/random/randomdev.c#L274-L285) for a seeded device: a request with `POLLIN` or `POLLRDNORM` gets only those bits back, and any other request gets every requested bit. At descriptors 0 to 2, regular files, directories and archived files use vnode readiness too, including host redirection (`< file`, `> log.txt` or `2> log.txt`). A standard descriptor that is not a socket, a kqueue, a random device or a vnode still takes the console fallback: descriptor 0 never reports an event and descriptors 1 and 2 report `POLLOUT`. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file, a directory, the random device nor descriptor 0 to 2 (other devices) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket, pipe or kqueue to wait on. A closed socket descriptor reports `POLLNVAL`; 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`. -- [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 +- [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - a guest socket has the SOL_SOCKET level, eight `IPPROTO_TCP` options, `IPV6_V6ONLY` and ten `IPPROTO_IP` options. `SO_ACCEPTFILTER` (0x1000) fails with `EINVAL` for a set and a get on a socket that is not listening, as a payload process on a PS5 (firmware 13.60) did and as FreeBSD 11.4's `do_setopt_accept_filter` and `do_getopt_accept_filter` do; on a listening socket both throw, as accept filters are not modelled; the other options of these levels fail with `ENOPROTOOPT` and were not all measured. `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and a boolean one reads back as its own bit, as there. Winsock does not clear a pending error when `SO_ERROR` reads it, so on Windows a pending error is reported once and again only after a new `connect`; a second failure with the same code without a `connect` in between is hidden - [TCP_NOPUSH, TCP_NOOPT, TCP option 0x80, IP_RECVDSTADDR, IP_RECVIF, IP_RECVTTL](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept and read back as a payload process on a PS5 (firmware 13.60) did: the TCP ones as the bits 0x1000, 0x8 and 0x10000 (so 0x80 is not FreeBSD's `TCP_KEEPINIT` there), the IP ones as 0 or 1. None of them changes what the socket does: `TCP_NOPUSH` does not hold back sends, and the IP ones add no control message to `recvmsg`. `IP_RECVTTL` was only read, never set, on the console, and an accepted socket takes `TCP_NOPUSH` from its listener as FreeBSD 11.4's `syncache_socket` does, which was not measured -- [SO_DEBUG, SO_REUSEADDR, SO_USELOOPBACK, SO_REUSEPORT, SO_TIMESTAMP, SO_BINTIME and option 0x10000](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept as bits that read back as on a PS5 (firmware 13.60, payload process), and options 0x4000 and 0x8000 are accepted and cannot be read, as there; only `SO_REUSEADDR` 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 +- [SO_DEBUG, SO_REUSEADDR, SO_USELOOPBACK, SO_REUSEPORT, SO_TIMESTAMP, SO_BINTIME and option 0x10000](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept as bits that read back as on a PS5 (firmware 13.60, payload process), and options 0x4000 and 0x8000 are accepted and cannot be read, as there; only `SO_REUSEADDR`, `SO_REUSEPORT`, `SO_TIMESTAMP` and `SO_BINTIME` change what the socket does. `SO_REUSEADDR` and `SO_REUSEPORT` are set on the host, `SO_REUSEPORT` as `SO_REUSEADDR` on Windows, which has no `SO_REUSEPORT` and there also lets the socket bind over one that did not set it; Linux spreads unicast datagrams across the sockets, where FreeBSD gives each to one. Option 0x10000 is only kept: if it is FreeBSD 12's `SO_REUSEPORT_LB`, two sockets that set it share a port there, where the second `bind` here fails with `EADDRINUSE`. An accepted socket takes the bits from its listener, as FreeBSD 11.4's `sonewconn` copies `so_options`; that was not measured - [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 @@ -260,10 +269,11 @@ - [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 +- [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. On PS5 firmware 13.60 it returns what `fcntl` returns there, and `0x80020000` plus the errno, which it also sets, on failure; this was measured for `F_GETFD`, `F_SETFD`, `F_GETFL`, `F_DUPFD`, `F_DUPFD_CLOEXEC`, `F_GETLK`, an unknown command and bad descriptors, not for the other commands - [Creating through a trailing slash](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) - `open` and `sceKernelOpen` with `O_CREAT` on a missing name followed by a slash (`new/`) fail with `EINVAL` on Windows and with the host's `EISDIR` on Linux, where FreeBSD 11.4's `lookup()` (sys/kern/vfs_lookup.c) fails with `ENOENT` because only a directory may be created through a trailing slash -- [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 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. +- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_DUPFD` returns the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_FSYNC` set by `open` or `F_SETFL` makes each `write`, `pwrite`, `writev` and `pwritev` on a file sync it before returning, as `vn_write` adds `IO_SYNC`; the `sceKernelAioSubmitWriteCommands` writes are not synced, as whether the console's AIO honours the flag is unknown; on Linux a file opened with it keeps the host `O_SYNC` after `F_SETFL` clears it. `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console +- [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_DUPFD`, `F_DUP2FD`, `F_DUP2FD_CLOEXEC`, `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL`, fails `F_DUPFD_CLOEXEC` with `EOPNOTSUPP` as a PS5 does, and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2`, `F_DUPFD`, `F_DUP2FD` onto another descriptor and an accepted socket start without it, `F_DUP2FD` onto the same descriptor keeps it, and `SOCK_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. +- [F_DUPFD_CLOEXEC and F_DUP2FD_CLOEXEC](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) - `F_DUPFD_CLOEXEC` on a shared memory descriptor fails with `EOPNOTSUPP`, where PS5 firmware 13.60 returns 0 for any start without setting `errno`; whether it duplicates anything is not known, since descriptor 0 was already open. `F_DUP2FD_CLOEXEC` returns the lowest free descriptor, where the console picks scattered free numbers below 1024 (17 to 1012 measured). Otherwise both follow the console: `F_DUPFD_CLOEXEC` fails with `EOPNOTSUPP` for any start on files, sockets, pipes and kqueues, after `EBADF` for a bad descriptor; `F_DUP2FD_CLOEXEC` ignores its target, which it neither opens nor closes (-1, -2, `INT_MAX`, the source and an open descriptor alike), and returns a new descriptor for the same open file without `FD_CLOEXEC`, or `EBADF` for a bad source - [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other well-formed request throws. An input request with length 0 is not rejected as malformed: [`sys_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L703-L718) rejects it only in a kernel built without `COMPAT_FREEBSD4`, `COMPAT_FREEBSD5` and `COMPAT_43`, and which of these the PS5 kernel has is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) 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` @@ -271,8 +281,8 @@ - [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 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 @@ -280,8 +290,10 @@ - [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_setname_np](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) - the console passes `name` to the kernel's `namedobj_create` and stores the returned id instead of the name; here the name is copied and never read back. Once named, a mutex returns `EAGAIN` (35) for every later call and keeps its name, as [firmware 13.42's module run on a PC](https://github.com/boykopovar/AnyPS5/pull/10385#issuecomment-6415603047) does. The kernel's handling of a null `name` is unmeasured: here it returns 0 and leaves the mutex unnamed. A null mutex pointer faults in the module and throws here; a statically initialized or destroyed mutex also throws, and the module's result for those is unmeasured. The module's separate branch for a field holding `0x5eaf00d`, which copies the name into the object, is known only from disassembly and is not reproduced +- [pthread_rwlock_setname_np](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - the console passes `name` to the kernel's `namedobj_create` and stores the returned id instead of the name; here the name is copied and never read back. Once named, a lock returns `EAGAIN` (35) for every later call and keeps its name, as [firmware 13.42's module run on a PC](https://github.com/boykopovar/AnyPS5/pull/10385#issuecomment-6415603047) does. The kernel's handling of a null `name` is unmeasured: here it returns `EINVAL`, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/kernel/threads/rwlock.cpp#L79-L87). A null lock pointer, a statically initialized lock and a destroyed one fault in the module and throw here +- [pthread_barrier_setname_np](../../../core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp) (libkernel) - the console passes `name` to the kernel's `namedobj_create` and stores the returned id instead of the name; here the name is copied and never read back. Once named, a barrier returns `EAGAIN` (35) for every later call and keeps its name, as [firmware 13.42's module run on a PC](https://github.com/boykopovar/AnyPS5/pull/10385#issuecomment-6415603047) does. The kernel's handling of a null `name` is unmeasured: here it returns 0 and leaves the barrier unnamed. A null barrier pointer, an unset barrier and a destroyed one fault in the module and throw here - [pthread_mutex_reltimedlock_np](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) takes `(mutex, usec)`, a relative timeout in microseconds, and is `scePthreadMutexTimedlock` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadMutexTimedlock` ([`mutex.cpp`](https://github.com/shadps4-emu/shadPS4/blob/7c960eae136b956cf4c1d72aacd2426b650330df/src/core/libraries/kernel/threads/mutex.cpp#L284-L287)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `pthread_cond_reltimedwait_np`, so a count of 2^32 microseconds or more would be truncated if the console reads 64 bits. Unverified on the console - [sysctl kern.arandom](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) serves FreeBSD 11's MIB {1, 37} (CTL_KERN, KERN_ARANDOM) and the "kern.arandom" sysctlbyname lookup through std::random_device, returning min(*oldLength, 256) random bytes. Following FreeBSD's sysctl_kern_arnd, a null buffer reports min(*oldLength, 256) without writing, a null length returns 0 without writing, and requests with a new value return EPERM (1) as the node is read-only. Cryptographic quality depends on the host's random device implementation. - [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 has a source-level call from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), but current-main runtime reachability is unverified @@ -291,12 +303,13 @@ - [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 - [fchflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) has the gaps of `chflags`: a valid non-zero flag throws, and the console's filesystem types, `EROFS` and the change-time update are not modelled. Character devices, the random device, other host descriptors and files read from app0.zar throw, since their console filesystems and the privilege check for devices are not modelled. On Windows, anonymous shared memory objects are temporary files that are not told apart from regular files, so they are treated as files instead of reporting `EINVAL` as FreeBSD 11.4's [`getvnode`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L3850-L3877) does. -- [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 +- [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: `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`, leaving errno unchanged on success. `setpriority` fails the same way; `PRIO_PROCESS` returns 0 without storing, so `getpriority` stays 0, as PS5 firmware 13.60 did in a payload process for sets of -25, -5, 5 and 25. A valid `PRIO_PGRP` or `PRIO_USER` set throws: unverified. What a title process reads, and the host nice of the process or of the threads `APS5_THREAD_PRIORITY=high` raises, are not reported or 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. - [sceKernelGetProcessAbiType](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) throws: no measured value, prototype or documented consumer of the process ABI type was found, and PS5_MihawkOS (`mihawk-99/PS5_MihawkOS`, PPSA99170) is the only title known to import it. Returning a guessed constant would let a title branch on a value the console never answered with, so the declaration only resolves the import until the console value is measured - [flock on a directory descriptor](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) returns 0 on Windows without taking a lock, as FreeBSD 11.4's `sys_flock` accepts any vnode descriptor and so succeeds on a directory; Windows `LockFileEx` rejects a directory handle, which used to abort the process. Two descriptors of the same directory therefore never conflict: a second `LOCK_EX | LOCK_NB` succeeds where FreeBSD fails with `EWOULDBLOCK`. Linux and macOS use the host `flock`. Not measured on a console +- [poll on macOS and urgent data](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - XNU's `poll` never reports `POLLPRI` for out-of-band data. On macOS it is taken from `SIOCATMARK`, which reports a pending urgent byte without consuming it, and a wait that asks for it uses the sliced path a kqueue wait uses, so it wakes on the next 1 ms slice; `POLLRDBAND` is set with it, as FreeBSD 11.4's `sopoll_generic` does. The other paths that ask the host for urgent readiness still miss it on macOS: guest `select` and `pselect` through `GuestSockets::Poll`, and libSceNet's `PollSockets`. `recv(MSG_OOB | MSG_PEEK)` is not the probe because it consumes the byte on macOS 27.0.1, and `kevent` with `NOTE_OOB` does not fire there. Every other flag and error comes from `poll`; every other host is unchanged. The guest `send` and `recv` refuse `MSG_OOB`, so urgent data can only come from an outside peer; the console's stop once the normal stream passes the mark was not reproduced diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index fd691b6f0d..358952bc73 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -15,7 +15,7 @@ - [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` @@ -23,11 +23,13 @@ - [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 +- [uuidgen](../../../core/libs/prx/libkernel_sys/Uuid.cpp) (libkernel_sys) - the node is 48 random bits with the multicast bit set, chosen once per process, where the console's kernel uses the address of its first Ethernet interface; that the PS5 kernel registers one is assumed, not measured. Otherwise it follows FreeBSD 11's [`kern_uuidgen` and `sys_uuidgen`](https://github.com/freebsd/freebsd-src/blob/53c4471833e8e5e57b4d1b501b1a52b343bb0451/sys/kern/kern_uuid.c#L129-L198): version 1 UUIDs with consecutive timestamps from the guest `CLOCK_REALTIME`, a random 14-bit clock sequence that is incremented when the clock has not passed the last batch, and `EINVAL` for a count outside 1 to 2048. A null store returns `EFAULT` after the state advances, as `copyout` fails there; other invalid addresses are not detected - [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) 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 diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index d91f5cd055..ee4775c342 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -10,17 +10,19 @@ - [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. +- 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. -- `--frame-red-zone` frames only leaf functions that start with a chain of pushes of at least five bytes (`push rbp; mov rbp, rsp; push ...`, or pushes alone, which may be followed by `sub $n, %rsp` or `lea -n(%rsp), %rsp`) and leave through exits of at least five bytes that undo that allocation with `add` or `lea`, pop the same registers and `ret` ([framer](../../../core/relinker/codegen/src/x86/RedZoneFramer.cpp)). Leaves with a frame pointer that allocate, leaves with neither pushes nor an allocation, shorter prologues and exits, exits that free another size, and functions with indirect jumps or other RSP arithmetic keep their red zone. +- `--frame-red-zone` frames only leaf functions that start with pushes (`push rbp; mov rbp, rsp; push ...`, or pushes alone followed or not by a `sub` or `lea` allocation) or with neither pushes nor an allocation, and leave through exits that undo the allocation, pop the same registers and `ret` ([framer](../../../core/relinker/codegen/src/x86/RedZoneFramer.cpp)). A prologue or exit shorter than its five-byte jump takes in the straight-line instructions after or before it. The leaf keeps its red zone when those cannot make five bytes, when a branch targets one of them or the first instruction of a widened prologue, or when a leaf without pushes pushes or pops: that refuses fragments that pop registers pushed before them, and shrink-wrapped leaves that push later, whose pushes would land on their own spills once RSP is lowered. Leaves with a frame pointer that allocate, exits that free another size and functions with indirect jumps or other RSP arithmetic keep their red zone too. - In a red-zone leaf without a frame pointer, `--frame-red-zone` lowers RSP after the prologue by the deepest displacement below RSP, rounded up to 16, and moves every RSP operand by the same amount ([framer](../../../core/relinker/codegen/src/x86/RedZoneFramer.cpp)). A leaf that takes the address of a slot below RSP, or uses an RSP operand with an index register, is lowered by the whole 128 bytes, since a pointer or an index can reach deeper than any displacement; values below RSP reached through any other register are not seen. Leaves with a frame pointer are lowered by 128 bytes. Inside a moved frame the CFA rules of the guest's unwind tables are off by the shift. - An RSP operand that `--frame-red-zone` cannot move in place, an 8-bit displacement the shift takes past 0x7f or one without a displacement, moves to a stub with the instructions after it up to five bytes and is re-encoded there with the moved displacement ([framer](../../../core/relinker/codegen/src/x86/RedZoneFramer.cpp)). The leaf is skipped when that site would take in a branch target, a branch, ret or RIP-relative instruction, or another site, and when the operand is EVEX, whose 8-bit displacement is scaled. A game patch over a framed site or a moved displacement is refused. - In `--frame-red-zone`, function bounds come from the executable's `.eh_frame` FDEs: each function is decoded from its FDE start to its FDE end, or to int3 padding before it, and the relinker stops with an error when the executable has none. Code outside every FDE, such as a function without unwind information, is not framed ([framer](../../../core/relinker/codegen/src/x86/RedZoneFramer.cpp)). - `--to-intel` lowers MCOMMIT to STC plus NOP padding and RDPRU through a runtime-checked stub in the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp): AMD APM Vol. 3 defines RDPRU ECX 0 as MPERF and ECX 1 as APERF, neither readable on Intel hosts, so both selectors return the host TSC as a disclosed monotonic-counter approximation (their ratio always reads 1.0) while other selectors yield zero EDX:EAX. STC asserts CF=1 because under x86-TSO prior stores are already ordered and guest memory is host-DRAM-backed, leaving the commit-success case as the only observable one. Non-zero RDPRU selectors, the TSC-for-MPERF/APERF substitution, and any console path reporting MCOMMIT CF=0 are unverified. - [Unscoped imports on Windows](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) - an import without a module that no searched library exports under its name is searched again under the NID of that name, because homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) imports by symbol name (Quake, from the websrv homebrew launcher; Mari0, #6239) while the libraries in `libs/` export NIDs. A dependency named with `.sprx`, `.suprx` or `.debug_prx` loads the `.prx` library of the same stem from `libs/`, and `libkernel_web` also loads `libkernel.prx`, as `libSceLibcInternal` already loads `libc.prx`: AnyPS5's `libkernel_web.prx` exports only two functions, where the console's `libkernel_web.sprx` is a libkernel variant. A name that matches a library's export as written still binds to that export first. macOS resolves these imports through the host loader by name and is unchanged. - [Unscoped imports on Linux](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) - in an executable or bundled module that names a dependency with `.sprx`, `.suprx` or `.debug_prx`, as homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) does, an import without a module that is named by symbol is bound to the NID of that name only. Unlike Windows, the name as written is not tried first, because the host loader also searches the host C library, which exports `malloc` or `pthread_create` under their names. A name that a bundled module exports still binds to that module, as does a bundled module's import of a name the executable exports, and such a dependency loads the `.prx` library of the same stem. Unqualified imports of other executables and bundled modules are still bound by name. +- An `R_X86_64_GLOB_DAT` or `R_X86_64_JUMP_SLOT` relocation with a nonzero addend fails the conversion in the Linux output ([ELF patcher](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp)), in the Windows and macOS outputs ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)) and in guest modules ([guest image reader](../../../core/relinker/relinker/src/guest/GuestImageReader.cpp)). What the console's loader does with such an addend is not measured. The x86-64 psABI computes both as S, but loaders differ: FreeBSD rtld uses S for GLOB_DAT and lazy JUMP_SLOT and S+A for eager JUMP_SLOT, and glibc used S+A for eager binding before [f8587a61892c](https://github.com/bminor/glibc/commit/f8587a61892cbafd98ce599131bf4f103466f084) (2.36) and S since. - The length-changing path of the [instruction rewriter](../../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. The rewriter conservatively rejects length-changing edits when a surviving truncated relative branch would move relative to its target (only one of the two lies after the cut) or when its section-relative target falls outside the 16-bit range, because the runtime address base needed to resolve a wrapped destination is unavailable; truncated branches that keep their distance and stay in range are accepted. - The [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp) takes the branch targets that keep an instruction in place from a linear scan of the code segment, so data in that segment decodes as branches too: OpenSSL's x86-64 assembly keeps strings such as "SHA1 block transform for x86_64, CRYPTOGAMS by " in its code, and in SuperTuxKart (PPSA69421) and Mortalfin (PPSA99141) the `jae` read from "sf" points into the VEX prefix of a 256-bit store. A direct branch target inside a scanned instruction counts only where every byte before it is a legacy prefix or REX, which a real branch can skip (a `lock` prefix, for example); a target in a VEX prefix, opcode or operand is ignored, as a jump-table entry that starts no instruction already is. A real branch to such a target, overlapping instructions as hand-written code can, is not seen, and the instruction it enters can be moved. - A `PT_SCE_RELRO` segment that no `PT_LOAD` covers, the PS4 layout, is [loaded](../../../core/relinker/relinker/src/parsing/ElfReader.cpp) as a read-write `PT_LOAD` and stays writable after relocation; FreeBSD rtld makes `PT_GNU_RELRO` read-only once relocation is done, and what the console does with `PT_SCE_RELRO` is not checked. @@ -30,6 +32,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 +42,8 @@ - `--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. + +- Relative pointer-width unwind pointer decoding in the [exception frame reader](../../../core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp) (`DW_EH_PE_absptr` and `DW_EH_PE_udata8`) wraps modulo 2^64 to accommodate backward offsets, as standard DWARF unwinders add them. Console unwind table validation for out-of-range displacements is unmeasured. Unsigned offsets narrower than pointer width (`DW_EH_PE_udata2` and `DW_EH_PE_udata4`) cannot express negative displacements and throw when address addition overflows. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 6c372caccf..e7aba24b21 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -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. @@ -190,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 2fc449c0d1..d10adeb020 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -14,7 +14,7 @@ Select or drop multiple app ZIP archives to convert them into one ZIP bundle. Th The SELF unwrapper refuses containers requiring more than 64 MiB of total alignment padding before allocating the output buffer. This limit counts padding across file-backed and empty segments, not segment data; a refusal preserves the input and any existing output file. -Use a clean ELF executable. Place its bundled ELF modules in `sce_module/`, `sce_modules/`, or `prx/` beside the input executable, or under the parent directory selected with `--sce-module-path `. The option accepts absolute paths and paths relative to the current working directory; do not include the `sce_module` directory itself. Only files directly inside the module directories are processed. SELF/fSELF containers are skipped with a warning; unwrap them before relinking if they need to be included. `tools/unwrap_self.py ` does that for a decrypted container: it places the segments at the offsets of the SELF segment records and restores `p_offset % p_align == p_vaddr % p_align`, and it refuses a container whose load segments all look encrypted. The command-line relinker skips an ELF module as a backup copy, with a message, when its file name extends the name of another ELF module in the same directory and both have the same SONAME, such as a dumper's `libX.prx.esbak` or `libX.prxbak` beside an ELF `libX.prx`; a single backup beside a SELF container is still relinked. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present is an error. All three absent is an error unless an explicit `--module-dir` is supplied. +Use a clean ELF executable. Place its bundled ELF modules in `sce_module/`, `sce_modules/`, or `prx/` beside the input executable, or under the parent directory selected with `--sce-module-path `. The option accepts absolute paths and paths relative to the current working directory; do not include the `sce_module` directory itself. Only files directly inside the module directories are processed. SELF/fSELF containers are skipped with a warning; unwrap them before relinking if they need to be included. `tools/unwrap_self.py ` does that for a decrypted container: it places the segments at the offsets of the SELF segment records and restores `p_offset % p_align == p_vaddr % p_align`, and it refuses a container whose load segments all look encrypted. The relinker, on the command line and in the online converter, skips an ELF module as a backup copy, with a message, when its file name extends the name of another ELF module in the same directory and both have the same SONAME, such as a dumper's `libX.prx.esbak` or `libX.prxbak` beside an ELF `libX.prx`; a single backup beside a SELF container is still relinked. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present is an error. All three absent is an error unless an explicit `--module-dir` is supplied. ```text source/ @@ -51,6 +51,8 @@ relinker --macos source/input.elf eboot Add `--to-intel` for Intel hosts, or `--to-rosetta` for Rosetta; a title that runs under Rosetta on Apple silicon is relinked with `--macos --to-rosetta`. `--to-intel` rewrites AMD-specific instructions found in the executable and bundled modules; it cannot translate AMD-specific instructions generated by a title at runtime, so such titles require an AMD host. The output format defaults to Linux ELF regardless of the filename; `.exe` alone does not select Windows. +A converted title is an ordinary program: it runs with your user account's permissions and can read and write whatever your account can, with none of the console's sandbox. Convert and run only titles and modules you trust, and run them as a separate user or in a container or virtual machine to keep them away from your files. Its executable is not placed by address space layout randomization either: Linux loads it at `0x400000` or at the `--linux-fixed-base` address, unless the deprecated `--lazy-binding` keeps it position independent; Windows loads it at its preferred base `0x140000000` when that range is free, or at the `--windows-fixed-base` address, without `DYNAMIC_BASE`; and the macOS executable is written without `MH_PIE`. + Titles run natively, so the host CPU needs the extensions of the PS5's Zen 2 CPU that compiled code uses without checking, among them AVX2, FMA, F16C, BMI1, BMI2, LZCNT and MOVBE. On Linux and Windows a converted title logs the host CPU's extensions at start-up and stops before running any title code, naming the CPU and the missing extensions, if one is absent ([list](../dev/debt/libkernel.md)). The executable, registry, Windows import diagnostics, icon and desktop entry output files must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. These collisions fail before any output is written. An existing output file can be replaced if it is separate from those inputs. @@ -67,7 +69,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--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 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 and Linux output only; conflicts with `--lazy-binding`. | +| `--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. | @@ -76,7 +78,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`. | @@ -89,7 +91,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--exclude-sce-module ` | Deprecated. Exclude a bundled module by exact filename, not path. Repeat for multiple files; a missing filename is an error. Conflicts with `--skip-sce-module`. | | `--skip-syscall-check` | Deprecated. Disable syscall scanning in the executable and bundled modules. | | `--trap-sdk-syscall-stubs` | Replace the exact ps5-payload-dev generic and numbered syscall wrapper stubs and the recognized inline mmap/mprotect sequences from SDK libc/mman.c with `UD2`, so unused wrappers do not block relinking. Other syscall instructions still cause an error. If a trapped wrapper runs, it raises an illegal-instruction exception. For debugging only. | -| `--frame-red-zone` | Windows only: give leaf functions that keep values below RSP (the System V red zone) a frame, so an exception Windows delivers on a guest thread cannot overwrite them. Covers leaves with a `push rbp; mov rbp, rsp; push ...` prologue and leaves that only push registers, with or without a `sub` allocation after the pushes; in those, RSP is lowered only as far as the leaf reaches below it and their RSP operands are moved with it, in place or, when their encoding cannot hold the moved displacement, in a stub. Function bounds come from the executable's `.eh_frame`, and the relinker stops with an error when it has none. The relinker prints how many it framed and why it skipped the others, and refuses a game patch over a framed site. | +| `--frame-red-zone` | Windows only: give leaf functions that keep values below RSP (the System V red zone) a frame, so an exception Windows delivers on a guest thread cannot overwrite them. Covers leaves with a `push rbp; mov rbp, rsp; push ...` prologue, leaves that only push registers, with or without a `sub` allocation after the pushes, and leaves with neither; a prologue or exit shorter than a jump takes in the instructions next to it. In leaves without a frame pointer, RSP is lowered only as far as the leaf reaches below it and their RSP operands are moved with it, in place or, when their encoding cannot hold the moved displacement, in a stub. Function bounds come from the executable's `.eh_frame`, and the relinker stops with an error when it has none. The relinker prints how many it framed and why it skipped the others, and refuses a game patch over a framed site. | | `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. The Linux executable stays position independent (`ET_DYN`) instead of being placed at `0x400000`. | Specify `unused-filter=0|1|2` without `--`, at most once. Unknown options and extra positional arguments are errors. Use `relinker --help` to print the usage syntax to stdout and exit with code `0` without converting files. Invoking `relinker` without arguments prints the usage syntax to stderr and exits with code `1`. @@ -105,7 +107,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: @@ -122,7 +124,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. @@ -229,7 +233,7 @@ A Windows executable started without a standard output or error handle, such as ### Display resolution and window size -The game chooses its video-output resolution; AnyPS5 uses that mode for rendering. The display window can be resized or switched to fullscreen, which scales the presented image to fit the window. Changing the window size does not change the game's render resolution. AnyPS5 does not currently provide a global render-resolution override; use a game's own display setting when it offers one. +The game chooses its video-output resolution; AnyPS5 uses that mode for rendering. The display window can be resized or switched to fullscreen, which scales the presented image to fit the window. F11 switches between window and fullscreen; set `ANYPS5_FULLSCREEN` to a non-empty value other than `0` to open the window fullscreen from the start. Changing the window size does not change the game's render resolution. AnyPS5 does not currently provide a global render-resolution override; use a game's own display setting when it offers one. ### VR compatibility @@ -346,7 +350,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): @@ -368,7 +372,7 @@ Limits are listed in the [libc](../dev/debt/libc.md) and [relinker](../dev/debt/ ### Performance overlay -F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERLAY=1` shows it from the first frame. `ANYPS5_OVERLAY_POS` puts it in a corner of the window: `tl` (top left, the default), `tr`, `bl` or `br`. For either variable an empty value is the same as an unset one, and any other value stops the game at its first frame with an error that names the variable. The key can be moved with `TogglePerformanceOverlay` in the [input mapping](INPUT_MAPPING.md). The text scales with the window height, at a whole multiple of its 5x7 pixel font; on an HDR10 swapchain the overlay is drawn at 203 nits. +F10 cycles a performance overlay over the presented image through three states: off, page 1 (the table below), page 2 (a detailed page, the second table), then off again. `ANYPS5_OVERLAY=1` shows page 1 from the first frame and `ANYPS5_OVERLAY=2` page 2; F10 then goes on from there. `ANYPS5_OVERLAY_POS` puts it in a corner of the window: `tl` (top left, the default), `tr`, `bl` or `br`. For either variable an empty value is the same as an unset one, and any other value stops the game at its first frame with an error that names the variable. The key can be moved with `TogglePerformanceOverlay` in the [input mapping](INPUT_MAPPING.md). The text scales with the window height, at a whole multiple of its 5x7 pixel font; on an HDR10 swapchain the overlay is drawn at 203 nits. | Line | Meaning | |------|---------| @@ -387,6 +391,28 @@ F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERL Times are shown with one decimal below 100 and as whole numbers above; anything above 9999 shows as 9999, so `FRAME` and `MIN/AVG/MAX` read 9999 for frames of 10 s or more. `FPS` has the same format, with two decimals below 1. Counts are whole numbers, in thousands (`12K`) from 10000 and in millions (`3M`) from a million. Memory is in GB of 2^30 bytes. While the overlay is hidden the presentation records nothing for it and the presented image is unchanged; while it is shown, one small render pass draws it over each presented frame. Each draw and dispatch packet and each pipeline creation is counted also while the overlay is hidden, with one increment of a counter that belongs to the packet worker or, for pipelines, a shared one; the memory and the counters are read only while the overlay is shown. +Page 2 is wider, 46 characters, and has no graph. It reads its sources once a second while it is shown, so its rates cover the last second; they appear a second after the page does. A line whose source isn't available in the run is left out, and so is a part of a line. + +| Line | Meaning | +|------|---------| +| `FPS`, `FRAME` | As on page 1. | +| `BUSY %` | The share of the last second each queue's packet worker (`Q0` for queue 0, `Q1` for queue 1, and so on) spent on packets, suspends, ends of submissions, rewinds and completions, as `worker_busy_pct` of the trace summary counts it. From the trace totals. | +| phases | The 5 phases with the most CPU time per frame over the last second, in ms, summed over the driver's threads and named as in the trace summary's `top_phases_ms_per_frame`: a timed scope, or a scope and one of its stages. A scope's time includes the scopes and stages within it, and threads that work at once each count, so the phases can add up to more than `FRAME`. From the trace totals. | +| `Q0 WAIT MS/FRAME` | Time per frame that queue 0's packet worker waited, in ms: `MEM` in packets that wait for guest memory or for rendering to finish, `LOCK` for the GPU lock, `GPU` in the recorder's syncs and serial waits and in command batches' fence waits. A memory wait that syncs the GPU counts in `MEM` and in `GPU`. From the trace totals. | +| `RENDER PASSES/FRAME` | Render passes begun per frame; a draw that continues the open pass begins none. From the trace totals; not shown while an `APS5_TRACE_*` variable is set. | +| `PIPELINES` | `COMPILING`: shader preparations and pipeline-library prebuilds queued or running. `NEW/S`: pipelines created in the last second, as `SHADERS` on page 1. | +| `HIT %` | `DRAW CACHE`: the share of the draw cache's lookups in the last second that found an entry whose stages all matched. `PIPELINES`: the share of the pipeline cache's lookups that found the pipeline. Each is left out for a second without lookups. | +| `FLIPS/S`, `PRESENTS/S` | The title's flips per second against the frames presented per second. | +| `DROPPED/S`, `REPEATED/S` | Per second: flips not presented because no swapchain image was free (the `dropped` count of the `[present]` line of `APS5_PROFILE_DRAW=1`), and presents of the same display buffer as the present before. | +| `COMMIT` | The system's commit charge against its commit limit, in GB: `CommitTotal` and `CommitLimit` of `GetPerformanceInfo` on Windows, `Committed_AS` and `CommitLimit` of `/proc/meminfo` on Linux, where the limit is enforced only with `vm.overcommit_memory=2`. Then `FAULTS/S`, the process's page faults per second, soft ones included: `PageFaultCount` of `GetProcessMemoryInfo` on Windows, minor plus major faults of `getrusage` on Linux. | +| `HOST IMPORTS` | Host memory imports in use, then their size against the import budget, in GB. Not shown on a device that can't import host memory. | +| `CPU COPIES` | Guest memory copied per second by address-based resource builds, for the ranges that no host import or mirror serves, in MB of 2^20 bytes. `CAP`: the largest of those builds in the last second against the copy cap, `APS5_ADDRESS_COPY_MAX_MIB` (64 MiB unless set), past which a build stops the game. | +| `UNIMPLEMENTED` | Calls per second to functions AnyPS5 doesn't implement, then their total since the start. Such a call throws an error, so a count above 0 means the title caught it and went on. | +| `REFUSED SHADERS` | Shaders whose draws are skipped. Shown only with `APS5_SKIP_REFUSED_DRAWS=1`; without it a refused shader stops the game. | +| `TITLE`, `PRESENT` | The title ID from the title's `param.json`, and the swapchain's present mode, as `ANYPS5_PRESENT_MODE` names it. | + +The busy shares, the phases, the queue 0 waits and the render passes come from the totals the trace keeps per thread. Showing page 2 turns those totals on, without the event buffers of an `APS5_TRACE_FRAMES` trace: while page 2 is shown, each timed scope on the driver's threads reads the clock twice and adds its time to its thread's totals, and each render pass begun is counted there. Leaving page 2 turns the totals off again. A run with an `APS5_TRACE_*` variable keeps its trace as configured; page 2 then shows those lines only while that trace records. While the overlay is hidden or on page 1, page 2 adds a check per render pass begun (the same check each timed scope already makes), a per-thread count of the bytes each address-based build copies, a count of unimplemented-function calls and the display buffer's address per present; its sources are read only while it is shown. + ### Windows crash diagnostics When a register points at readable memory that starts with at least four printable ASCII characters, either as UTF-16LE or as single bytes, the crash report prints that text after the register memory dump, as `[r13] u"..."` for UTF-16LE or `[rdi] "..."` for single bytes. The text ends at a NUL, at the first character that is not printable ASCII, a tab, a line feed or a carriage return, after 512 characters or at the end of readable memory. Tabs, line breaks, quotes and backslashes are escaped, and `...` after the closing quote means that no NUL ended the text within those limits. diff --git a/tools/annotate_module.py b/tools/annotate_module.py new file mode 100644 index 0000000000..b57b34178a --- /dev/null +++ b/tools/annotate_module.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +import argparse +import re +import shutil +import struct +import subprocess +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import nid_names + +PT_LOAD = 1 +PT_DYNAMIC = 2 +PF_X = 1 +PF_W = 2 +DT_NULL = 0 +DT_HASH = 4 +DT_STRTAB = 5 +DT_SYMTAB = 6 +DT_RELA = 7 +DT_RELASZ = 8 +DT_JMPREL = 23 +DT_PLTRELSZ = 2 +DT_PLTREL = 20 +DT_STRSZ = 10 +DT_RELAENT = 9 +DT_REL = 17 +DT_RELA_KIND = 7 +STT_TLS = 6 +DT_GNU_HASH = 0x6ffffef5 +SYMBOL_SIZE = 24 +RELA_SIZE = 24 + +LINE_ADDRESS = re.compile(r"^\s*([0-9a-f]+):") +STUB = re.compile(r"^\s*([0-9a-f]+):\s+(?:\w+\s+)?jmpq?\s+\*(?:-?0x[0-9a-f]+)?\(%rip\)\s+# 0x([0-9a-f]+)") +BRANCH = re.compile(r"\b(?:call|jmp)q?\s+0x([0-9a-f]+)") +REFERENCE = re.compile(r"# 0x([0-9a-f]+)") + + +class ModuleError(Exception): + pass + + +class Module: + def __init__(self, data): + self.data = data + self.loads = [] + self.dynamic = None + self.read_headers() + self.tags = self.read_tags() + self.strtab = self.address(self.require_tag(DT_STRTAB, "DT_STRTAB")) + self.symtab = self.address(self.require_tag(DT_SYMTAB, "DT_SYMTAB")) + self.strsz = self.require_tag(DT_STRSZ, "DT_STRSZ") + if self.strtab + self.strsz > len(self.data): + raise ModuleError("the string table of DT_STRSZ %#x extends past the end of the file" % self.strsz) + self.symbol_count = self.count_symbols() + self.check_relocation_tags() + + def unpack(self, form, offset): + size = struct.calcsize(form) + if offset < 0 or offset + size > len(self.data): + raise ModuleError("read of %d bytes at file offset %#x is outside the file" % (size, offset)) + return struct.unpack_from(form, self.data, offset) + + def read_headers(self): + if len(self.data) < 64 or self.data[:4] != b"\x7fELF": + raise ModuleError("not an ELF file") + if self.data[4] != 2 or self.data[5] != 1: + raise ModuleError("not a 64-bit little-endian ELF") + if self.unpack("= self.symbol_count: + raise ModuleError("symbol index %d is outside the %d symbols of the module" % (index, self.symbol_count)) + name, info, _, shndx, value, _ = self.unpack("= self.strsz: + raise ModuleError("symbol %d has a name offset %#x outside DT_STRSZ %#x" % (index, name, self.strsz)) + start = self.strtab + name + end = self.data.find(b"\0", start, self.strtab + self.strsz) + if end < 0: + raise ModuleError("symbol %d has a name unterminated within DT_STRSZ %#x" % (index, self.strsz)) + return self.data[start:end].decode("utf-8", errors="replace"), shndx, value, info & 0xF + + def check_relocation_tags(self): + if DT_REL in self.tags: + raise ModuleError("DT_REL relocations are not supported") + if DT_RELA in self.tags: + self.require_tag(DT_RELASZ, "DT_RELASZ") + if DT_JMPREL in self.tags: + self.require_tag(DT_PLTRELSZ, "DT_PLTRELSZ") + if self.require_tag(DT_PLTREL, "DT_PLTREL") != DT_RELA_KIND: + raise ModuleError("DT_PLTREL is not DT_RELA") + if self.tags.get(DT_RELAENT, RELA_SIZE) != RELA_SIZE: + raise ModuleError("DT_RELAENT is not %d" % RELA_SIZE) + + def relocation_tables(self): + if DT_RELA in self.tags: + yield self.tags[DT_RELA], self.tags[DT_RELASZ] + if DT_JMPREL in self.tags: + yield self.tags[DT_JMPREL], self.tags[DT_PLTRELSZ] + + def imports(self): + slots = {} + for table, size in self.relocation_tables(): + if size % RELA_SIZE: + raise ModuleError("relocation table at %#x has %d bytes, not a multiple of %d" % (table, size, RELA_SIZE)) + start = self.address(table) + for position in range(start, start + size, RELA_SIZE): + offset, info, _ = self.unpack("> 32: + slots[offset] = self.symbol(info >> 32)[0] + return slots + + def exports(self): + found = {} + thread_local = {} + for index in range(1, self.symbol_count): + name, shndx, value, kind = self.symbol(index) + if shndx == 0 or not name: + continue + (thread_local if kind == STT_TLS else found).setdefault(value, name) + return found, thread_local + + def code_segments(self): + segments = [] + for vaddr, offset, size, flags in sorted(self.loads): + if flags & PF_X: + if offset + size > len(self.data): + raise ModuleError("the executable PT_LOAD segment at %#x extends past the end of the file" % vaddr) + segments.append((vaddr, self.data[offset:offset + size])) + if not segments: + raise ModuleError("no executable PT_LOAD segment") + return segments + + +class Namer: + def __init__(self, db): + self.db = db + + def resolve(self, symbol): + nid = symbol.split("#", 1)[0] + name = nid_names.strip_nid_postfix(self.db.get(nid, "")) + return name if name and nid_names.compute_nid(name) == nid else nid + + +OBJDUMP_NAMES = ("objdump", "gobjdump", "x86_64-linux-gnu-objdump", "x86_64-elf-objdump") + + +def gnu_objdump(): + for name in OBJDUMP_NAMES: + path = shutil.which(name) + if path is None: + continue + result = subprocess.run([path, "--version"], capture_output=True, text=True, encoding="utf-8", errors="replace") + if result.returncode == 0 and result.stdout.startswith("GNU objdump"): + return path + return None + + +def disassemble(code, vaddr): + objdump = gnu_objdump() + if objdump is None: + raise ModuleError("GNU binutils objdump was not found as any of %s" % ", ".join(OBJDUMP_NAMES)) + with tempfile.TemporaryDirectory() as directory: + blob = Path(directory) / "text.bin" + blob.write_bytes(code) + command = [objdump, "-D", "-b", "binary", "-m", "i386:x86-64", "--no-show-raw-insn", + "--adjust-vma=%d" % vaddr, str(blob)] + result = subprocess.run(command, capture_output=True, text=True, encoding="utf-8", errors="replace") + if result.returncode != 0: + raise ModuleError("objdump failed: %s" % result.stderr.strip()) + return result.stdout.splitlines() + + +def find_stubs(listing, slots, namer): + stubs = {} + for line in listing: + match = STUB.match(line) + if match and int(match.group(2), 16) in slots: + stubs[int(match.group(1), 16)] = namer.resolve(slots[int(match.group(2), 16)]) + return stubs + + +def line_note(line, slots, stubs, namer): + note = "" + branch = BRANCH.search(line) + if branch and int(branch.group(1), 16) in stubs: + note = " ; -> " + stubs[int(branch.group(1), 16)] + reference = REFERENCE.search(line) + if reference and int(reference.group(1), 16) in slots: + note = " ; GOT " + namer.resolve(slots[int(reference.group(1), 16)]) + return note + + +def annotate_listing(listing, slots, exports, stubs, namer): + out = [] + for line in listing: + head = LINE_ADDRESS.match(line) + if head and int(head.group(1), 16) in exports: + out.append("") + out.append("; ===== export " + namer.resolve(exports[int(head.group(1), 16)])) + out.append(line + line_note(line, slots, stubs, namer)) + return out + + +def write_lines(path, lines): + with open(path, "w", encoding="utf-8", newline="\n") as handle: + for line in lines: + handle.write(line + "\n") + + +def export_lines(exports, thread_local, namer): + lines = ["%#x %s %s" % (address, exports[address], namer.resolve(exports[address])) for address in sorted(exports)] + lines += ["tls+%#x %s %s" % (offset, thread_local[offset], namer.resolve(thread_local[offset])) for offset in sorted(thread_local)] + return lines + + +def annotate(module_path, prefix, namer): + module = Module(Path(module_path).read_bytes()) + slots = module.imports() + exports, thread_local = module.exports() + listing = [] + for vaddr, code in module.code_segments(): + listing.append("; ===== segment %#x" % vaddr) + listing.extend(disassemble(code, vaddr)) + stubs = find_stubs(listing, slots, namer) + write_lines(prefix + ".imports.txt", ["%#x %s %s" % (slot, slots[slot], namer.resolve(slots[slot])) for slot in sorted(slots)]) + write_lines(prefix + ".exports.txt", export_lines(exports, thread_local, namer)) + write_lines(prefix + ".asm", annotate_listing(listing, slots, exports, stubs, namer)) + return "%s: %d imports, %d exports, %d tls, %d stubs, %d lines" % (prefix, len(slots), len(exports), len(thread_local), len(stubs), len(listing)) + + +def parse_arguments(argv): + parser = argparse.ArgumentParser( + description="Annotated disassembly of a relinked guest module (no section headers). Writes PREFIX.imports.txt, " + "PREFIX.exports.txt and PREFIX.asm, with imports, PLT stubs, GOT references and exports named " + "through the NID database. Requires GNU binutils objdump (objdump, or gobjdump from Homebrew binutils).") + parser.add_argument("module", help="relinked guest module, for example libc.prx.guest.prx") + parser.add_argument("--out", help="output prefix (default: the module path)") + parser.add_argument("--db", help="NID database CSV (downloaded to a cache dir by default)") + parser.add_argument("--no-names", action="store_true", help="print NIDs without loading the database") + return parser.parse_args(argv) + + +def main(argv=None): + args = parse_arguments(sys.argv[1:] if argv is None else argv) + if gnu_objdump() is None: + sys.exit("GNU binutils objdump is required but none of %s in PATH is GNU objdump" % ", ".join(OBJDUMP_NAMES)) + try: + db = {} if args.no_names else nid_names.load_db(Path(args.db) if args.db else nid_names.DEFAULT_CACHE) + print(annotate(args.module, args.out or args.module, Namer(db))) + except (ModuleError, OSError, UnicodeError) as error: + sys.exit("%s: %s" % (args.module, error)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) 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/check_compile.py b/tools/check_compile.py index 24500ab954..24daf7321a 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -11,7 +11,8 @@ from contextlib import nullcontext from pathlib import Path CPP = (".cpp", ".cc", ".cxx", ".c") -DROP = ("-c", "-MD", "-MMD", "-MP", "-fsyntax-only", "-fno-syntax-only") +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", diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 8503faf71d..b6b9a25b67 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -401,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()}") diff --git a/tools/check_main_sync.py b/tools/check_main_sync.py index a8be7392f2..cc35cad442 100644 --- a/tools/check_main_sync.py +++ b/tools/check_main_sync.py @@ -59,10 +59,10 @@ def report(repo, base, head): 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 e2f8f246f0..936570e00e 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -32,11 +32,13 @@ def high_refresh_limitation(output): 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") @@ -49,6 +51,8 @@ def skips(path): 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): @@ -59,9 +63,10 @@ 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: diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index d29473a451..518a885bd4 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -78,7 +78,9 @@ def commands(content): raise ValueError(f"invalid CTest argument at position {position}") argument = token.group(0) if argument.startswith('"'): - argument = re.sub(r"\\(.)", lambda match: {"n": "\n", "r": "\r", "t": "\t"}.get(match[1], match[1]), argument[1:-1], flags=re.S) + argument = re.sub(r"\\(\r?\n|.)", + lambda match: {"\n": "", "\r\n": "", "n": "\n", "r": "\r", "t": "\t"}.get(match[1], match[1]), + argument[1:-1], flags=re.S) elif token.group("equals") is not None: width = len(token.group("equals")) + 2 argument = argument[width:-width].removeprefix("\n") diff --git a/tools/configure_seed.py b/tools/configure_seed.py index 1dd9e52f29..6e817d8fae 100644 --- a/tools/configure_seed.py +++ b/tools/configure_seed.py @@ -18,6 +18,11 @@ def checks(cache): if match and match.group(2) == "INTERNAL" and CHECK.match(doc): found[match.group(1)] = (match.group(3), doc[2:]) doc = "" + architecture_sizes = {name for name, (value, doc) in found.items() + if value == "0" and doc.startswith("CHECK_TYPE_SIZE: ")} + if architecture_sizes: + return {name: entry for name, entry in found.items() + if name not in architecture_sizes and not entry[1].startswith("Result of TRY_COMPILE")} return found diff --git a/tools/debt_split.py b/tools/debt_split.py index 1460f46bca..ab66d0de76 100644 --- a/tools/debt_split.py +++ b/tools/debt_split.py @@ -149,6 +149,10 @@ def git(root, *args, check=True): def apply_change(root, base, new, index_text): + class Entry: + def __init__(self, text): + self.text = text + order = sections_of(index_text) or sections_of(new) before = [(kind, section, item, key(kind, item)) for kind, section, item in parse(base) if kind == "entry" and key(kind, item)] after = [(kind, section, item, key(kind, item)) for kind, section, item in parse(new) if kind == "entry" and key(kind, item)] @@ -156,51 +160,70 @@ def apply_change(root, base, new, index_text): def load(name): if name not in files: - files[name] = read_debt(root, name) + files[name] = {section: [Entry(text) for text in entries] + for section, entries in read_debt(root, name).items()} return files[name] - def locate(entry): - _, section, item, name = entry - entries = load(name).get(section, []) - moved = relink(item, INDEX, debt_file(name)) - if moved not in entries: - raise LookupError(f"{debt_file(name)}: entry not found under '{section}': {item.splitlines()[0][:100]}") - return entries, entries.index(moved) + matcher = difflib.SequenceMatcher(None, [entry[1:3] for entry in before], [entry[1:3] for entry in after], autojunk=False) + operations = matcher.get_opcodes() + names = {entry[3] for tag, i1, i2, j1, j2 in operations if tag != "equal" + for entry in before[i1:i2] + after[j1:j2]} + available = {} + for name in names: + for section, entries in load(name).items(): + for entry in entries: + available.setdefault((name, section, entry.text), []).append(entry) + available = {key: iter(entries) for key, entries in available.items()} + originals = [next(available.get((name, section, relink(item, INDEX, debt_file(name))), iter(())), None) + for _, section, item, name in before] + resolved = [None] * len(after) - def insert(entry, anchor): - _, section, item, name = entry + def locate(index): + _, section, item, name = before[index] + entries = load(name).get(section, []) + original = originals[index] + if original is None or original not in entries: + raise LookupError(f"{debt_file(name)}: entry not found under '{section}': {item.splitlines()[0][:100]}") + return entries, entries.index(original) + + def insert(index, anchor): + _, section, item, name = after[index] entries = load(name).setdefault(section, []) - moved = relink(item, INDEX, debt_file(name)) + moved = Entry(relink(item, INDEX, debt_file(name))) position = 0 if anchor is None else len(entries) - if anchor is not None and anchor[1] == section and anchor[3] == name: - anchored = relink(anchor[2], INDEX, debt_file(name)) + if anchor is not None: + anchored = resolved[anchor] if anchored in entries: position = entries.index(anchored) + 1 entries.insert(position, moved) + resolved[index] = moved - matcher = difflib.SequenceMatcher(None, [entry[1:3] for entry in before], [entry[1:3] for entry in after], autojunk=False) - for tag, i1, i2, j1, j2 in matcher.get_opcodes(): + for tag, i1, i2, j1, j2 in operations: if tag == "equal": + resolved[j1:j2] = originals[i1:i2] continue removed, added = before[i1:i2], after[j1:j2] kept_old, kept_new = set(), set() for offset, (old, fresh) in enumerate(zip(removed, added)): if old[1] == fresh[1] and old[3] == fresh[3]: - entries, position = locate(old) - entries[position] = relink(fresh[2], INDEX, debt_file(fresh[3])) + entries, position = locate(i1 + offset) + entries[position].text = relink(fresh[2], INDEX, debt_file(fresh[3])) + resolved[j1 + offset] = entries[position] kept_old.add(offset) kept_new.add(offset) for offset, old in enumerate(removed): if offset not in kept_old: - entries, position = locate(old) + entries, position = locate(i1 + offset) del entries[position] for offset, fresh in enumerate(added): if offset in kept_new: continue - anchor = next((entry for entry in reversed(after[:j1 + offset]) if entry[1] == fresh[1] and entry[3] == fresh[3]), None) - insert(fresh, anchor) + anchor = next((index for index in range(j1 + offset - 1, -1, -1) + if after[index][1] == fresh[1] and after[index][3] == fresh[3]), None) + insert(j1 + offset, anchor) for name, sections in files.items(): - write_debt(root, name, sections, order) + write_debt(root, name, {section: [entry.text for entry in entries] + for section, entries in sections.items()}, order) return sorted(debt_file(name) for name in files) diff --git a/tools/env_reference.py b/tools/env_reference.py index 2cfe032b71..d7f80595ff 100644 --- a/tools/env_reference.py +++ b/tools/env_reference.py @@ -91,8 +91,8 @@ def reads(roots, prefixes=PREFIXES): for path in source_files(root): try: text = path.read_text(encoding="utf-8") - except (OSError, UnicodeDecodeError): - continue + except (OSError, UnicodeDecodeError) as error: + raise ValueError(f"cannot read {path.as_posix()}: {error}") from error lines = text.splitlines() for number, line in enumerate(lines, 1): window = "\n".join(lines[number - 1:number + 2]) @@ -184,7 +184,11 @@ def main(argv=None): if not Path(root).is_dir(): print(f"error: scan root {root} is not a directory", file=sys.stderr) return 1 - found = reads(roots, prefixes) + try: + found = reads(roots, prefixes) + except ValueError as error: + print(f"error: {error}", file=sys.stderr) + return 1 if args.check: return check(found, args.doc, prefixes, sys.stdout) if args.table: diff --git a/tools/import_audit.py b/tools/import_audit.py index 03ab8499d3..57cf09a9b1 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -90,6 +90,7 @@ def elf_program_dynamic_symbols(path, data): _, _, dynamic_offset, _, _, dynamic_size, _, _ = dynamic_segments[0] if dynamic_size % 16: raise AuditError(f"{path}: dynamic segment size {dynamic_size} is not a multiple of 16") + take(data, dynamic_offset, dynamic_size, path, "dynamic segment") dynamic = {} needed_offsets = [] terminated = False @@ -126,16 +127,18 @@ def elf_program_dynamic_symbols(path, data): bucket_data = take(data, bucket_offset, buckets * 4, path, "GNU hash buckets") chain_offset = bucket_offset + buckets * 4 maximum = first_symbol - 1 + visited_chain_entries = set() for index in struct.unpack(f"<{buckets}I", bucket_data) if buckets else (): if index == 0: continue if index < first_symbol: raise AuditError(f"{path}: GNU hash bucket index {index} precedes symbol offset {first_symbol}") symbol_index = index - while True: + while symbol_index not in visited_chain_entries: chain_address = dynamic[0x6ffffef5] + (chain_offset - hash_offset) + (symbol_index - first_symbol) * 4 chain_file_offset = file_offset(chain_address, 4, "GNU hash chain") chain_value = unpack("= 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): @@ -218,6 +222,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 @@ -273,6 +278,8 @@ def pe_headers(path, data): def pe_exports(path, data): to_offset, directory, _ = pe_headers(path, data) rva, size = unpack(" 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..4cfc858e42 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() @@ -301,13 +301,31 @@ def overlaps(prs, a, b): return found if any(found.values()) else None + +def code_span(item): + item = str(item) + runs = re.findall(r"`+", item) + delimiter = "`" * (max((len(run) for run in runs), default=0) + 1) + if "`" in item: + return f"{delimiter} {item} {delimiter}" + return f"{delimiter}{item}{delimiter}" + + def cell(items): - return "
".join(f"`{item}`" for item in sorted(items or ())) + return "
".join(code_span(item) for item in sorted(items or ())) + + def spans(conflicts, blob): - return "
".join(f"`{path}` " + ", ".join(f"[L{s}-{e}]({blob}/{quote(path)}#L{s}-L{e})" if s != e else f"[L{s}]({blob}/{quote(path)}#L{s})" - for s, e in ranges) for path, ranges in sorted((conflicts or {}).items())) + return "
".join( + f"{code_span(path)} " + ", ".join( + f"[L{s}-{e}]({blob}/{quote(path)}#L{s}-L{e})" + if s != e else f"[L{s}]({blob}/{quote(path)}#L{s})" + for s, e in ranges + ) + for path, ranges in sorted((conflicts or {}).items()) + ) def pull(blob, number): 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/select_tests.py b/tools/select_tests.py index 8d3563274a..1fa013a3a9 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -169,6 +169,23 @@ 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 @@ -302,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) @@ -324,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 ead6b408e7..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): @@ -82,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): @@ -105,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: @@ -161,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 index f61b586e53..1536075235 100644 --- a/tools/symbolize_samples.py +++ b/tools/symbolize_samples.py @@ -237,7 +237,7 @@ def subtract(later, earlier): def window(later, earlier): result = Samples() - result.headers = {**earlier.headers, **later.headers} + 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 diff --git a/tools/tests/test_annotate_module.py b/tools/tests/test_annotate_module.py new file mode 100644 index 0000000000..bb01a2b717 --- /dev/null +++ b/tools/tests/test_annotate_module.py @@ -0,0 +1,259 @@ +import contextlib +import io +import re +import shutil +import struct +import sys +import tempfile +import subprocess +import unittest +from pathlib import Path +from unittest import mock + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import annotate_module +import nid_names + +IMPORT_NAME = "sceKernelGetProcessTime" +EXPORT_NAME = "sceKernelUsleep" +SECOND_IMPORT_NAME = "sceKernelGetTscFrequency" +TLS_NAME = "sceKernelGetCurrentCpu" +GNU_HASH = 0x6FFFFEF5 +CODE_ADDRESS = 0x1000 +DATA_ADDRESS = 0x2000 +SLOT = 0x2800 +CALL_ADDRESS = 0x1006 +EXPORT_ADDRESS = 0x100B + + +def nid(name): + return nid_names.compute_nid(name) + + +def symbol_name(name): + return nid(name) + "#A#A" + + +def code_bytes(prefix=b""): + stub = prefix + b"\xff\x25" + struct.pack(" " + IMPORT_NAME)) + header = listing.index("; ===== export " + EXPORT_NAME) + self.assertEqual(instruction(listing, "ret"), listing[header + 1]) + self.assertEqual([], [path.name for path in self.root.iterdir() if path.suffix == ".bin"]) + + def test_unknown_nid_prints_the_nid(self): + empty = self.root / "empty.csv" + empty.write_text("", encoding="utf-8") + self.run_tool(build_module(), "--db", str(empty)) + self.assertEqual("%#x %s %s\n" % (SLOT, symbol_name(IMPORT_NAME), nid(IMPORT_NAME)), self.read(".imports.txt")) + self.assertIn("; -> " + nid(IMPORT_NAME), self.read(".asm")) + + def test_no_names_skips_the_database(self): + self.run_tool(build_module(), "--no-names") + self.assertIn("; -> " + nid(IMPORT_NAME), self.read(".asm")) + + def test_mismatching_database_name_is_not_used(self): + wrong = self.root / "wrong.csv" + wrong.write_text("%s sceKernelOther\n" % nid(IMPORT_NAME), encoding="utf-8") + self.run_tool(build_module(), "--db", str(wrong)) + self.assertNotIn("sceKernelOther", self.read(".asm")) + + def test_gnu_hash_gives_the_symbol_count(self): + symbols = [(IMPORT_NAME, 0, 0x12, 0), (SECOND_IMPORT_NAME, 0, 0x12, 0), (EXPORT_NAME, 1, 0x12, EXPORT_ADDRESS)] + self.annotate(build_module(tags=(GNU_HASH, 5, 6, 10, 11, 23, 2, 20), symbols=symbols, kind="gnu")) + self.assertIn(EXPORT_NAME, self.read(".exports.txt")) + + def test_glob_dat_slot_referenced_by_an_indirect_call(self): + code = b"\xff\x15" + struct.pack(" " + IMPORT_NAME)) + + def test_tls_symbol_is_listed_but_not_an_address(self): + symbols = DEFAULT_SYMBOLS + [(TLS_NAME, 1, 0x16, EXPORT_ADDRESS)] + summary = self.annotate(build_module(symbols=symbols)) + self.assertIn("1 exports, 1 tls", summary) + self.assertIn("tls+%#x %s %s\n" % (EXPORT_ADDRESS, symbol_name(TLS_NAME), TLS_NAME), self.read(".exports.txt")) + self.assertNotIn("export " + TLS_NAME, self.read(".asm")) + + def test_export_at_address_zero_is_kept(self): + symbols = [(IMPORT_NAME, 0, 0x12, 0), (EXPORT_NAME, 1, 0x12, 0)] + self.annotate(build_module(symbols=symbols)) + self.assertEqual("0x0 %s %s\n" % (symbol_name(EXPORT_NAME), EXPORT_NAME), self.read(".exports.txt")) + + def test_every_executable_segment_is_disassembled(self): + self.annotate(build_module(extra_code=b"\xc3")) + listing = self.read(".asm").splitlines() + first, second = listing.index("; ===== segment 0x1000"), listing.index("; ===== segment 0x3000") + self.assertLess(first, second) + self.assertTrue(any(re.match(r"^\s*3000:\s+ret\b", line) for line in listing[second:])) + + def test_missing_hash_fails(self): + self.assert_fails(build_module(tags=(5, 6, 10, 11, 23, 2, 20)), "neither DT_HASH nor DT_GNU_HASH") + + def test_missing_symtab_fails(self): + self.assert_fails(build_module(tags=(4, 5, 10, 11, 23, 2, 20)), "DT_SYMTAB") + + def test_missing_strsz_fails(self): + self.assert_fails(build_module(tags=(4, 5, 6, 11, 23, 2, 20)), "DT_STRSZ") + + def test_missing_pltrelsz_fails(self): + self.assert_fails(build_module(tags=(4, 5, 6, 10, 11, 23, 20)), "DT_PLTRELSZ") + + def test_missing_relasz_fails(self): + self.assert_fails(build_module(tags=(4, 5, 6, 10, 11, 7)), "DT_RELASZ") + + def test_pltrel_must_be_rela(self): + self.assert_fails(build_module(values={20: 17}), "DT_PLTREL is not DT_RELA") + + def test_dt_rel_is_rejected(self): + self.assert_fails(build_module(tags=DEFAULT_TAGS + (17,)), "DT_REL relocations are not supported") + + def test_relaent_must_be_24(self): + self.assert_fails(build_module(tags=DEFAULT_TAGS + (9,), values={9: 16}), "DT_RELAENT") + + def test_name_offset_past_strsz_fails(self): + self.assert_fails(build_module(values={10: len(symbol_name(IMPORT_NAME)) + 2}), "symbol 2 has a name offset") + + def test_strsz_past_the_end_of_the_file_fails(self): + self.assert_fails(build_module(values={10: 0x100000}), "string table of DT_STRSZ 0x100000 extends past the end of the file") + + def test_name_unterminated_within_strsz_fails(self): + self.assert_fails(build_module(values={10: 2}), "unterminated within DT_STRSZ") + + def test_relocation_size_not_a_multiple_of_24_fails(self): + self.assert_fails(build_module(values={2: 25}), "not a multiple of 24") + + def test_no_executable_segment_fails(self): + self.assert_fails(build_module(code_flags=6), "no executable PT_LOAD segment") + + def test_wrong_machine_fails(self): + self.assert_fails(build_module(machine=0xB7), "not an x86-64 ELF") + + +if __name__ == "__main__": + unittest.main() 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_check_compile.py b/tools/tests/test_check_compile.py index 6bbbbf5570..d1963387b3 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -15,7 +15,49 @@ import check_compile TOOL = Path(__file__).resolve().parents[1] / "check_compile.py" -class CheckCompileTests(unittest.TestCase): +class CheckCompileFixture(unittest.TestCase): + def setUp(self): + holder = tempfile.TemporaryDirectory() + self.addCleanup(holder.cleanup) + self.root = Path(holder.name).resolve() + subprocess.run(["git", "init", "-q", str(self.root)], check=True) + subprocess.run(["git", "-C", str(self.root), "config", "user.email", "tests@example.com"], check=True) + subprocess.run(["git", "-C", str(self.root), "config", "user.name", "tests"], check=True) + self.build = self.root / "build" + self.build.mkdir() + + def tool(self, *arguments, env=None): + environment = dict(os.environ) + environment.update(env or {}) + return subprocess.run([sys.executable, "-I", str(TOOL), *arguments], cwd=self.root, + capture_output=True, text=True, encoding="utf-8", env=environment) + + def source(self, relative, text="int x;\n"): + path = self.root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + return path + + def compiler(self, body, name="compiler.py"): + script = self.root / name + script.write_text(body, encoding="utf-8") + return script + + def entry(self, relative, command): + return {"directory": str(self.build), "command": command, "file": str(self.root / relative)} + + def manifest(self, entries): + (self.build / "compile_commands.json").write_text(json.dumps(entries), encoding="utf-8") + + def failing(self, relative, message=None): + source = self.source(relative) + message = message if message is not None else f"{relative}:1:1: error: no\n" + script = self.compiler("import sys\nsys.stderr.write(%r)\nsys.exit(1)\n" % message) + self.manifest([self.entry(relative, f'"{sys.executable}" "{script}" "{source}"')]) + return source + + +class CheckCompileOutputTests(CheckCompileFixture): def test_unexpanded_response_files_cannot_rewrite_build_outputs(self): compiler = shutil.which("clang") if compiler is None: @@ -79,6 +121,58 @@ class CheckCompileTests(unittest.TestCase): for path, content in outputs.items(): self.assertEqual(path.read_bytes(), content) + def test_saved_temporaries_do_not_rewrite_files_or_hide_compiler_errors(self): + compiler = shutil.which("gcc") or shutil.which("clang") + if compiler is None: + self.skipTest("needs a C compiler") + source = self.source("unit.c") + for option, field in (("-save-temps", "arguments"), ("--save-temps", "command"), + ("-save-temps=cwd", "arguments"), ("-save-temps=obj", "command")): + with self.subTest(option=option, field=field): + source.write_text("#ifndef REQUIRED\n#error missing definition\n#endif\nint value;\n", encoding="utf-8") + arguments = [compiler, "-Werror", "-DREQUIRED", option, "-c", str(source), + "-o", str(self.build / "unit.o")] + original = subprocess.run(arguments, cwd=self.build, capture_output=True, text=True) + self.assertEqual(original.returncode, 0, original.stderr) + for name in ("a-unit", "a-unit.i", "unit.i"): + (self.build / name).write_bytes(b"previous build output") + command = (subprocess.list2cmdline(arguments) if os.name == "nt" else + " ".join(shlex.quote(argument) for argument in arguments)) + self.manifest([{"directory": str(self.build), "file": str(source), + field: arguments if field == "arguments" else command}]) + preserved = {path: path.read_bytes() for path in self.build.iterdir()} + result = self.tool("unit.c") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("1 translation units compile", result.stdout) + for path, content in preserved.items(): + self.assertEqual(path.read_bytes(), content, str(path)) + self.assertEqual(set(self.build.iterdir()), set(preserved)) + source.write_text("#error syntax_check_must_fail\n", encoding="utf-8") + result = self.tool("unit.c") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("syntax_check_must_fail", result.stderr) + for path, content in preserved.items(): + self.assertEqual(path.read_bytes(), content, str(path)) + self.assertEqual(set(self.build.iterdir()), set(preserved)) + + def test_rejected_saved_temporary_mode_is_not_hidden(self): + compiler = shutil.which("gcc") or shutil.which("clang") + if compiler is None: + self.skipTest("needs a C compiler") + source = self.source("unit.c") + original = subprocess.run([compiler, "-save-temps=invalid", "-fsyntax-only", str(source)], + cwd=self.build, capture_output=True, text=True) + if original.returncode == 0: + self.skipTest("compiler accepts unknown saved-temporary modes with -fsyntax-only") + self.assertIn("save-temps", original.stderr) + self.manifest([{"directory": str(self.build), "file": str(source), + "arguments": [compiler, "-save-temps=invalid", "-c", str(source), + "-o", str(self.build / "unit.o")]}]) + result = self.tool("unit.c") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("save-temps", result.stderr) + self.assertFalse((self.build / "unit.o").exists()) + def test_ninja_response_file_is_expanded_before_syntax_rewriting(self): compiler = shutil.which("clang") or shutil.which("gcc") if compiler is None or shutil.which("ninja") is None: @@ -149,46 +243,7 @@ class CheckCompileTests(unittest.TestCase): self.assertEqual(fragment.read_bytes(), b"previous compilation fragment") - def setUp(self): - holder = tempfile.TemporaryDirectory() - self.addCleanup(holder.cleanup) - self.root = Path(holder.name).resolve() - subprocess.run(["git", "init", "-q", str(self.root)], check=True) - subprocess.run(["git", "-C", str(self.root), "config", "user.email", "tests@example.com"], check=True) - subprocess.run(["git", "-C", str(self.root), "config", "user.name", "tests"], check=True) - self.build = self.root / "build" - self.build.mkdir() - - def tool(self, *arguments, env=None): - environment = dict(os.environ) - environment.update(env or {}) - return subprocess.run([sys.executable, "-I", str(TOOL), *arguments], cwd=self.root, - capture_output=True, text=True, encoding="utf-8", env=environment) - - def source(self, relative, text="int x;\n"): - path = self.root / relative - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(text, encoding="utf-8") - return path - - def compiler(self, body, name="compiler.py"): - script = self.root / name - script.write_text(body, encoding="utf-8") - return script - - def entry(self, relative, command): - return {"directory": str(self.build), "command": command, "file": str(self.root / relative)} - - def manifest(self, entries): - (self.build / "compile_commands.json").write_text(json.dumps(entries), encoding="utf-8") - - def failing(self, relative, message=None): - source = self.source(relative) - message = message if message is not None else f"{relative}:1:1: error: no\n" - script = self.compiler("import sys\nsys.stderr.write(%r)\nsys.exit(1)\n" % message) - self.manifest([self.entry(relative, f'"{sys.executable}" "{script}" "{source}"')]) - return source - +class CheckCompileTests(CheckCompileFixture): def test_a_translation_unit_that_does_not_compile_fails_with_the_compiler_output(self): self.failing("unit.cpp") result = self.tool("unit.cpp") 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 85292d1d51..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") diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index 9c708417ee..567a04612c 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -345,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 ae68754774..d26cdd035e 100644 --- a/tools/tests/test_debt_split.py +++ b/tools/tests/test_debt_split.py @@ -224,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 7035c0615c..518fd2f165 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -56,7 +56,7 @@ def build_elf(symbols, needed=()): return bytes(image) -def build_sectionless_elf(symbols, needed=(), gnu_hash=False): +def build_sectionless_elf(symbols, needed=(), gnu_hash=False, gnu_buckets=(1,)): strings = b"\0" entries = [struct.pack("`normal.cpp`", + ) + + + def test_conflict_spans_with_backticks(self): + result = pr_overlap.spans( + {"example`file.cpp": [(5, 5)]}, + "https://github.com/boykopovar/AnyPS5/blob/main", + ) + + self.assertIn( + "`` example`file.cpp ``", + result, + ) + self.assertIn( + "example%60file.cpp#L5", + result, + ) + class CommentStateTests(unittest.TestCase): def setUp(self): if os.name == "nt" or not all(shutil.which(tool) for tool in ("bash", "jq", "sed", "sha256sum")): diff --git a/tools/tests/test_pr_overlap_dependencies.py b/tools/tests/test_pr_overlap_dependencies.py index 9af379f868..fdab9124e6 100644 --- a/tools/tests/test_pr_overlap_dependencies.py +++ b/tools/tests/test_pr_overlap_dependencies.py @@ -129,6 +129,22 @@ class DependencyTests(unittest.TestCase): self.assertIsNone(pr_overlap.overlaps(prs, 1, 2)) self.assertIsNone(pr_overlap.overlaps(prs, 2, 1)) + def test_reported_link_round_trips_as_a_dependency(self): + with patch.object(pr_overlap, "REPO", "owner/repo", create=True): + link = pr_overlap.pull("https://github.com/owner/repo/blob/abc123", 2) + self.assertIn("https://www.github.com/owner/repo/pull/2", link) + prs = pull_requests(f"Depends on: {link}") + self.assertEqual(prs[1]["depends"], {2}) + for pr in prs.values(): + pr["added"] = {"shared.cpp"} + self.assertIsNone(pr_overlap.overlaps(prs, 1, 2)) + + def test_www_github_dependencies_are_supported(self): + for reference in ("https://www.github.com/owner/repo/pull/2", + "[PR 2](https://www.github.com/owner/repo/pull/2)"): + with self.subTest(reference=reference): + self.assertEqual(pull_requests(f"Depends on: {reference}")[1]["depends"], {2}) + def test_unrelated_reference_keeps_overlap(self): prs = pull_requests("Related to #2.") for pr in prs.values(): diff --git a/tools/tests/test_pr_overlap_encoding.py b/tools/tests/test_pr_overlap_encoding.py index ea60e5b076..2e6f412edc 100644 --- a/tools/tests/test_pr_overlap_encoding.py +++ b/tools/tests/test_pr_overlap_encoding.py @@ -43,10 +43,12 @@ def command_output(stdout, stderr="", returncode=0): def run(command, **kwargs): if command[:3] == ["git", "rev-parse", "--show-toplevel"]: - return RUN(command, **kwargs) + root = (str(Path.cwd()) + "\n").encode("utf-8") + return subprocess.CompletedProcess(command, 0, stdout=root, stderr=b"") return RUN([sys.executable, "-c", code], **kwargs) with simulated_cp1252(), \ + patch.dict(pr_overlap._REPO_ROOTS, clear=True), \ patch.object(subprocess, "run", side_effect=run): yield diff --git a/tools/tests/test_progress.py b/tools/tests/test_progress.py index c7f305b811..1c8e33b4d7 100644 --- a/tools/tests/test_progress.py +++ b/tools/tests/test_progress.py @@ -597,12 +597,13 @@ class ProgressTests(unittest.TestCase): self.assertIn(str(path), stderr.getvalue()) def test_output_directory_that_cannot_be_written_is_named(self): + libraries, shaders = self.library_snapshot() with tempfile.TemporaryDirectory() as directory: blocked = Path(directory) / "blocked output" blocked.write_text("a file, not a directory", encoding="utf-8") stderr = io.StringIO() with patch("sys.stderr", stderr), self.assertRaises(SystemExit) as raised: - progress.main([str(blocked)]) + self.run_main([str(blocked)], libraries, shaders) self.assertEqual(raised.exception.code, 2) self.assertIn(str(blocked), stderr.getvalue()) self.assertNotIn("Traceback", stderr.getvalue()) diff --git a/tools/tests/test_progress_history.py b/tools/tests/test_progress_history.py index 7dbec9aa67..caa5492c5b 100644 --- a/tools/tests/test_progress_history.py +++ b/tools/tests/test_progress_history.py @@ -104,7 +104,8 @@ class ProgressHistoryTests(unittest.TestCase): root = Path(directory) subprocess.run(["git", "-C", directory, "init", "-q"], check=True) commit(root, "2026-01-01", "first", "int APS5_VABI A() { return 0; }\n") - with patch.multiple(progress, ROOT=root, PRX=root / "core/libs/prx", OPCODES=root / OPCODES): + with patch.multiple(progress, ROOT=root, PRX=root / "core/libs/prx", OPCODES=root / OPCODES), \ + patch.dict(os.environ, {"LC_ALL": "C"}): with self.assertRaises(ValueError) as raised: progress_history.daily_commits("no-such-ref") self.assertIn("no-such-ref", str(raised.exception)) diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index 515dc16930..cbe8248990 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -111,6 +111,21 @@ class SelectTests(unittest.TestCase): def test_missing_graph_runs_everything(self): self.assertIsNone(self.select("alpha/alpha.h", graph=self.build / "missing.json")[1]) + def test_malformed_graph_is_reported_without_stale_selection(self): + self.select("alpha/alpha.h") + self.assertTrue((self.build / "select.tests").exists()) + graph = self.build / "invalid.json" + graph.write_text("{}", encoding="utf-8") + changed = self.build / "changed invalid.txt" + changed.write_text("alpha/alpha.h\n", encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), "select", str(self.build), str(graph), str(changed)], + capture_output=True, text=True, timeout=60) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn("invalid test graph", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertFalse((self.build / "select.tests").exists()) + self.assertFalse((self.build / "select.ninja").exists()) + def test_test_missing_from_the_graph_runs(self): graph = json.loads(self.graph.read_text(encoding="utf-8")) del graph["tests"]["beta"] @@ -331,6 +346,29 @@ class BuildErrorTests(unittest.TestCase): def run_tool(self, *args): return subprocess.run([sys.executable, "-I", str(TOOL), *map(str, args)], capture_output=True, text=True, timeout=60) + def test_explain_rejects_malformed_saved_graph_without_a_traceback(self): + graphs = ( + {}, + [], + {"tests": [], "closures": {}}, + {"tests": {}, "closures": []}, + {"tests": {"unit": {}}, "closures": {}}, + {"tests": {}, "closures": {"unit": "src/unit.cpp"}}, + {"tests": {"unit": {"target": None, "registered": [], "arguments": [], + "sets": [], "requires": [], "depends": [3]}}, "closures": {}}, + ) + with tempfile.TemporaryDirectory() as directory: + graph = Path(directory) / "graph.json" + changed = Path(directory) / "changed.txt" + changed.write_text("src/unit.cpp\n", encoding="utf-8") + for document in graphs: + with self.subTest(document=document): + graph.write_text(json.dumps(document), encoding="utf-8") + result = self.run_tool("explain", graph, changed) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn("invalid test graph", result.stderr) + self.assertNotIn("Traceback", result.stderr) + def test_missing_build_directory_is_reported_without_a_traceback(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) 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 4f9f5677ab..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 @@ -241,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 index 5e000527dc..2393d66480 100644 --- a/tools/tests/test_symbolize_samples.py +++ b/tools/tests/test_symbolize_samples.py @@ -191,6 +191,38 @@ class SymbolizeSamplesTests(unittest.TestCase): inclusive = lines[lines.index("hottest by inclusive:") + 1:] self.assertIn("6 100.0% libSceAgcDriver.prx!short", inclusive[0]) + def test_since_does_not_verify_a_module_using_only_the_earlier_record(self): + record = f"# module libSceAgcDriver.prx {SIZE_OF_IMAGE:#x} {TIMESTAMP:#x}\n" + for collapsed in (False, True): + with self.subTest(collapsed=collapsed): + earlier = self.root / "early.txt" + later = self.root / "late.txt" + if collapsed: + earlier.write_text(record + "libSceAgcDriver.prx+0x1011 4\n", encoding="utf-8") + later.write_text("libSceAgcDriver.prx+0x1011 7\n", encoding="utf-8") + else: + earlier.write_text(record + "# 4 samples\nlibSceAgcDriver.prx 0x1011 4 4\n", encoding="utf-8") + later.write_text("# 7 samples\nlibSceAgcDriver.prx 0x1011 7 7\n", encoding="utf-8") + lines, errors = self.run_main(later, "--since", earlier, "--libs", self.libs) + self.assertEqual(lines[0], "3 samples") + self.assertIn("libSceAgcDriver.prx+0x1011", "\n".join(lines)) + self.assertNotIn("libSceAgcDriver.prx!short", "\n".join(lines)) + self.assertIn("no '# module' record", errors) + lines, errors = self.run_main(later, "--since", earlier, "--libs", self.libs, "--unverified") + self.assertIn("libSceAgcDriver.prx!short", "\n".join(lines)) + self.assertEqual(errors, "") + + def test_since_keeps_the_later_module_identity_when_the_build_changes(self): + earlier = self.root / "early.txt" + later = self.root / "late.txt" + earlier.write_text(f"# module libSceAgcDriver.prx {SIZE_OF_IMAGE:#x} {TIMESTAMP:#x}\n" + "libSceAgcDriver.prx+0x1011 4\n", encoding="utf-8") + later.write_text(f"# module libSceAgcDriver.prx {SIZE_OF_IMAGE:#x} {TIMESTAMP + 1:#x}\n" + "libSceAgcDriver.prx+0x1011 7\n", encoding="utf-8") + lines, errors = self.run_main(later, "--since", earlier, "--libs", self.libs) + self.assertIn("libSceAgcDriver.prx+0x1011", "\n".join(lines)) + self.assertIn("timestamp", errors) + def test_malformed_lines_are_rejected_with_their_line_number(self): bad = self.root / "bad.collapsed.txt" bad.write_text("# header\nlibSceAgcDriver.prx+0x1011 many\n", encoding="utf-8") diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index 7a32e30221..a329b64cf2 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -15,6 +15,30 @@ import check_test_registration class RegistrationTests(unittest.TestCase): + @unittest.skipUnless(shutil.which("cmake"), "CMake is required for the argument oracle") + def test_quoted_continuations_follow_cmake(self): + for ending, backslashes in (("\n", 1), ("\r\n", 1), ("\n", 2), ("\n", 3), ("\n", 4)): + with self.subTest(ending=repr(ending), backslashes=backslashes), tempfile.TemporaryDirectory() as directory: + content = 'add_test("real" "tools/tests/test_' + '\\' * backslashes + ending + 'bounds.py")\n' + script = Path(directory) / "oracle.cmake" + script.write_bytes(('function(add_test)\n' + 'file(WRITE "${CMAKE_CURRENT_LIST_DIR}/argument.txt" "${ARGV1}")\n' + 'endfunction()\n' + content).encode()) + subprocess.run(["cmake", "-P", str(script)], capture_output=True, text=True, check=True) + actual = (Path(directory) / "argument.txt").read_text() + expected = "tools/tests/test_" + '\\' * (backslashes // 2) + ("\n" if backslashes % 2 == 0 else "") + "bounds.py" + self.assertEqual(actual, expected) + self.assertEqual(list(check_test_registration.commands(content)), [("add_test", ["real", actual])]) + if backslashes == 1: + (Path(directory) / "CMakeLists.txt").write_bytes(content.encode()) + self.assertEqual(check_test_registration.run_scripts(Path(directory), ["CMakeLists.txt"]), {actual}) + + def test_literal_escaped_newline_and_bracket_continuation_are_preserved(self): + quoted = r'add_test("real" "a\nb")' + '\n' + bracket = 'add_test("real" [=[a' + '\\' + '\n' + 'b]=])\n' + self.assertEqual(list(check_test_registration.commands(quoted)), [("add_test", ["real", "a\nb"])]) + self.assertEqual(list(check_test_registration.commands(bracket)), [("add_test", ["real", "a\\\nb"])]) + def test_bracket_arguments_ignore_only_one_initial_newline(self): values = (("path", "path"), ("\npath", "path"), ("\n\npath", "\npath"), ("\n path", " path"), (" \npath", " \npath"), ("\\npath", "\\npath"), diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index f3cdc26fe5..67b93b73c6 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -1,4 +1,5 @@ import contextlib +from functools import lru_cache import gzip import io import json @@ -46,6 +47,71 @@ SUMMARY = {"commit": "abc", "dirty": False, "env": {"APS5_TRACE_FRAMES": "600:3" STACKS = "# samples=5 depth=64 truncated=1\n# module a.prx 0x1000 0x1\nntdll.dll+0x1;a.prx+0x10;a.prx+0x20 3\nntdll.dll+0x1;a.prx+0x10;a.prx+0x30 1\n[truncated];a.prx+0x40;a.prx+0x30 1\n" +@lru_cache(maxsize=2) +def excessive_trace_json(opening, closing): + depth = sys.getrecursionlimit() + 64 + for _ in range(8): + value = opening * depth + "0" + closing * depth + try: + json.loads(value) + except RecursionError: + return opening * (depth + 128) + "0" + closing * (depth + 128) + depth *= 2 + raise AssertionError("JSON decoder accepted every bounded nesting probe") + + +class TraceJsonNestingTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.trace = Path(self.directory.name) / "baseline.json" + self.trace.write_text(json.dumps({"traceEvents": EVENTS}), encoding="utf-8") + + def test_excessive_trace_json_nesting_has_a_path_qualified_error(self): + for opening, closing in (("[", "]"), ('{"nested":', "}")): + data = ('{"traceEvents": [], "extra": ' + excessive_trace_json(opening, closing) + "}").encode("utf-8") + for compressed in (False, True): + with self.subTest(opening=opening, compressed=compressed): + path = Path(self.directory.name) / ("deep-\u00e9.json.gz" if compressed else "deep-\u00e9.json") + original = gzip.compress(data) if compressed else data + path.write_bytes(original) + with self.assertRaisesRegex(ValueError, "trace JSON nesting is too deep") as error: + trace_digest.load_trace(path) + self.assertIn(str(path), str(error.exception)) + self.assertIsInstance(error.exception.__cause__, RecursionError) + self.assertEqual(path.read_bytes(), original) + + def test_excessive_trace_json_nesting_produces_only_a_cli_error(self): + for opening, closing, compressed, args in (("[", "]", False, ()), ('{"nested":', "}", True, ("--json",)), + ("[", "]", True, ()), ('{"nested":', "}", False, ("--json",))): + with self.subTest(opening=opening, compressed=compressed, args=args): + path = Path(self.directory.name) / ("deep-\u00e9.json.gz" if compressed else "deep-\u00e9.json") + data = ('{"traceEvents": [], "extra": ' + excessive_trace_json(opening, closing) + "}").encode("utf-8") + original = gzip.compress(data) if compressed else data + path.write_bytes(original) + result = subprocess.run([sys.executable, trace_digest.__file__, str(path), *args], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertEqual(result.stdout, "") + self.assertIn(f"FAIL: {path}: trace JSON nesting is too deep", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(path.read_bytes(), original) + + def test_supported_nested_trace_metadata_preserves_the_digest(self): + expected = trace_digest.digest(trace_digest.load_trace(self.trace), SUMMARY, None, 5, 10) + metadata = {"value": 7} + for _ in range(16): + metadata = {"nested": [metadata]} + data = json.dumps({"traceEvents": EVENTS, "extra": metadata}).encode("utf-8") + for compressed in (False, True): + with self.subTest(compressed=compressed): + path = Path(self.directory.name) / ("nested.json.gz" if compressed else "nested.json") + original = gzip.compress(data) if compressed else data + path.write_bytes(original) + trace = trace_digest.load_trace(path) + self.assertEqual(trace_digest.digest(trace, SUMMARY, None, 5, 10), expected) + self.assertEqual(path.read_bytes(), original) + class TraceDigestTests(unittest.TestCase): def test_summary_jsonl_record_shapes_are_validated(self): path = Path(self.directory.name) / "invalid-summary.jsonl" diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 59d334cfc1..2946f5147f 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -90,6 +90,53 @@ def read_program_headers(path): class UnwrapTest(unittest.TestCase): + def test_unsupported_inner_elf_machines_are_refused(self): + for machine in (0, 3, 40, 183, 0x3E00, 0xFFFF): + with self.subTest(machine=machine): + container = bytearray(build_container(default_segments(), default_records(), default_payloads())) + struct.pack_into("', + '', + '', ''): + with self.subTest(body=body): + self.assertTrue(self.report('' + body + '')[1]) + + def test_invalid_or_duplicate_test_names_are_setup_errors(self): + for body in ('', ''): + with self.subTest(body=body), self.assertRaises(gate.GateError): + self.report('' + body + '') + + def test_new_ids_are_compared_per_test_and_counts_are_retained(self): + def iteration(side, test, messages): + return {"side": side, "iteration": 1, "returncode": 0, "problems": [], + "tests": {test: {"diagnostics": messages}}} + report = gate.compare([iteration("baseline", "probe", {"VUID-A-1": 1}), + iteration("candidate", "probe", {"VUID-A-1": 3})]) + self.assertEqual(report["exit_code"], 0) + self.assertEqual(report["counts"]["candidate"]["probe"]["VUID-A-1"], 3) + report = gate.compare([iteration("baseline", "other", {"VUID-A-1": 1}), + iteration("candidate", "probe", {"VUID-A-1": 1})]) + self.assertEqual(report["new_diagnostics"], {"probe": ["VUID-A-1"]}) + self.assertEqual(report["exit_code"], 1) + + def test_windows_is_refused_before_output_is_created(self): + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "not-created" + arguments = ["--baseline", directory, "a" * 40, "--candidate", directory, "b" * 40, + "--output", str(output), "--target", "fixture", "--tests", "probe", "--gpu", "fixture"] + with mock.patch.object(gate.os, "name", "nt"), contextlib.redirect_stderr(io.StringIO()): + with self.assertRaises(SystemExit) as error: + gate.main(arguments) + self.assertEqual(error.exception.code, 2) + self.assertFalse(output.exists()) + + def test_relative_command_files_follow_the_test_working_directory(self): + for mode in ("default", "absolute", "relative"): + for definition in (False, True): + with self.subTest(mode=mode, definition=definition), tempfile.TemporaryDirectory() as directory: + build = Path(directory).resolve() + working = build if mode == "default" else build / "working directory" + working.mkdir(exist_ok=True) + script = working / "probe.py" + script.write_text("original", encoding="utf-8") + argument = "-DINPUT=probe.py" if definition else "probe.py" + properties = [] if mode == "default" else [{ + "name": "WORKING_DIRECTORY", + "value": str(working if mode == "absolute" else working.relative_to(build)), + }] + tests = [{"name": "probe", "command": [sys.executable, argument], "properties": properties}] + before = gate.artifact_hashes(build, tests, []) + self.assertIn(str(script.resolve()), before) + script.write_text("changed", encoding="utf-8") + self.assertNotEqual(before, gate.artifact_hashes(build, tests, [])) + + +@unittest.skipUnless(os.name == "posix" and shutil.which("cmake") and shutil.which("ninja") and shutil.which("ctest"), "requires POSIX, CMake, Ninja and CTest") +class FixtureTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.root = Path(self.directory.name).resolve() + + def source(self, name, message="pass", test="probe", property_text=""): + source = self.root / name + source.mkdir() + program = 'import os, sys\nfrom pathlib import Path\n' + program += 'assert os.environ["ANYPS5_REQUIRE_VULKAN"] == "1"\n' + program += 'assert os.environ["ANYPS5_NO_SHADER_CACHE"] == "1"\n' + program += 'assert os.environ["VK_INSTANCE_LAYERS"] == "VK_LAYER_KHRONOS_validation"\n' + program += 'assert os.environ["ANYPS5_GPU"] == "fixture"\n' + if message == "skip": + program += 'print("skipped, synthetic unavailable device")\nsys.exit(77)\n' + elif message == "fail": + program += 'sys.exit(7)\n' + elif message == "mutate": + program += 'with Path(__file__).open("a") as output: output.write("\\n")\n' + elif message == "large": + program += 'print("Validation Error: [ VUID-Early-00001 ] synthetic")\n' + program += 'print("x" * 131072)\n' + program += 'print("Validation Error: [ VUID-Late-00002 ] synthetic")\n' + else: + program += 'print(' + repr(message) + ')\n' + (source / "probe.py").write_text(program, encoding="utf-8") + (source / "CMakeLists.txt").write_text( + 'cmake_minimum_required(VERSION 3.20)\nproject(gate_fixture NONE)\nenable_testing()\n' + 'add_custom_command(OUTPUT "${CMAKE_BINARY_DIR}/probe.py" COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_SOURCE_DIR}/probe.py" "${CMAKE_BINARY_DIR}/probe.py" DEPENDS "${CMAKE_SOURCE_DIR}/probe.py")\n' + 'add_custom_target(fixture DEPENDS "${CMAKE_BINARY_DIR}/probe.py")\n' + 'add_test(NAME ' + test + ' COMMAND "' + sys.executable + '" "${CMAKE_BINARY_DIR}/probe.py")\n' + 'set_tests_properties(' + test + ' PROPERTIES SKIP_RETURN_CODE 77 ' + property_text + ')\n', encoding="utf-8") + subprocess.run(["git", "init", "-q", str(source)], check=True) + subprocess.run(["git", "-C", str(source), "add", "."], check=True) + subprocess.run(["git", "-C", str(source), "-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", + "-c", "commit.gpgSign=false", "commit", "-qm", "fixture"], check=True) + return source, gate.git(source, "rev-parse", "HEAD") + + def execute(self, baseline, candidate, *extra): + output = self.root / "evidence" + arguments = ["--baseline", str(baseline[0]), baseline[1], "--candidate", str(candidate[0]), candidate[1], + "--output", str(output), "--target", "fixture", "--tests", ".*", "--gpu", "fixture", *extra] + with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): + code = gate.main(arguments) + report = json.loads((output / "report.json").read_text()) if (output / "report.json").is_file() else None + return code, report + + +class IntegrationTests(FixtureTests): + def test_pass_builds_both_targets_and_alternates_with_attribution(self): + code, report = self.execute(self.source("base"), self.source("head")) + self.assertEqual(code, 0, report) + self.assertEqual([entry["side"] for entry in report["iterations"]], ["baseline", "candidate", "candidate", "baseline"]) + self.assertEqual(report["status"], "complete") + for side in ("baseline", "candidate"): + files = report["builds"][side]["artifact_sha256"] + self.assertTrue(any(path.endswith("probe.py") for path in files)) + self.assertTrue((self.root / "evidence" / side / "iteration-1.xml").is_file()) + + def test_ctest_success_with_validation_errors_is_a_gate_failure(self): + code, report = self.execute(self.source("base"), self.source("head", "Validation Error: [ VUID-Fixture-00001 ] synthetic"), "--repeat", "1") + self.assertEqual(code, 1, report) + self.assertTrue(all(entry["returncode"] == 0 for entry in report["iterations"])) + self.assertEqual(report["new_diagnostics"], {"probe": ["VUID-Fixture-00001"]}) + + def test_ctest_success_with_missing_layer_is_a_gate_failure(self): + message = "WARNING: [Loader Message] layer was not found but was requested by env var VK_INSTANCE_LAYERS" + code, report = self.execute(self.source("base", message), self.source("head"), "--repeat", "1") + self.assertEqual(code, 1, report) + self.assertTrue(all(entry["returncode"] == 0 for entry in report["iterations"])) + self.assertTrue(report["problems"]) + + def test_missing_layer_without_severity_is_a_gate_failure(self): + message = "Layer VK_LAYER_KHRONOS_validation was not found but was requested by env var VK_INSTANCE_LAYERS" + code, report = self.execute(self.source("base"), self.source("head", message), "--repeat", "1") + self.assertEqual(code, 1, report) + + def test_large_passed_output_retains_early_and_late_validation_messages(self): + code, report = self.execute(self.source("base"), self.source("head", "large"), "--repeat", "1") + self.assertEqual(code, 1, report) + self.assertEqual(report["new_diagnostics"], {"probe": ["VUID-Early-00001", "VUID-Late-00002"]}) + + def test_configuration_like_text_does_not_create_a_validation_error(self): + text = "VUID-Fixture-00001 was not found but was requested by env var VK_INSTANCE_LAYERS" + code, report = self.execute(self.source("base"), self.source("head", property_text='ENVIRONMENT "GATE_CONFIGURATION_ECHO=' + text + '"'), "--repeat", "1") + self.assertEqual(code, 0, report) + self.assertIn(text, (self.root / "evidence/candidate/iteration-1.log").read_text()) + + def test_a_skipped_selected_test_is_not_accepted(self): + code, report = self.execute(self.source("base"), self.source("head", "skip"), "--repeat", "1") + self.assertEqual(code, 1, report) + self.assertTrue(report["problems"]) + + +class AttributionTests(FixtureTests): + def test_relative_script_mutation_invalidates_successful_ctest_results(self): + baseline, candidate = self.source("base"), self.source("head", "mutate") + cmake = candidate[0] / "CMakeLists.txt" + text = cmake.read_text(encoding="utf-8") + absolute = 'add_test(NAME probe COMMAND "' + sys.executable + '" "${CMAKE_BINARY_DIR}/probe.py")' + self.assertIn(absolute, text) + cmake.write_text(text.replace(absolute, 'add_test(NAME probe COMMAND "' + sys.executable + '" probe.py)'), encoding="utf-8") + subprocess.run(["git", "-C", str(candidate[0]), "-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", + "-c", "commit.gpgSign=false", "commit", "-qam", "relative fixture"], check=True) + candidate = candidate[0], gate.git(candidate[0], "rev-parse", "HEAD") + code, report = self.execute(baseline, candidate, "--repeat", "1") + self.assertTrue(all(iteration["returncode"] == 0 for iteration in report["iterations"]), report) + self.assertEqual(report["builds"]["candidate"]["tests"][0]["command"][1], "probe.py") + self.assertNotEqual((self.root / "evidence/candidate/build/probe.py").read_bytes(), (candidate[0] / "probe.py").read_bytes()) + self.assertEqual(code, 2, report) + self.assertIn("artifacts changed", report["error"]) + + def test_different_selected_names_stop_before_iterations(self): + code, report = self.execute(self.source("base"), self.source("head", test="other")) + self.assertEqual(code, 2, report) + self.assertEqual(report["iterations"], []) + self.assertIn("test-name sets differ", report["error"]) + + def test_changed_artifact_invalidates_completed_test_results(self): + code, report = self.execute(self.source("base"), self.source("head", "mutate"), "--repeat", "1") + self.assertEqual(code, 2, report) + self.assertIn("artifacts changed", report["error"]) + + def test_test_properties_cannot_disable_required_validation(self): + code, report = self.execute(self.source("base"), self.source("head", property_text='ENVIRONMENT "ANYPS5_REQUIRE_VULKAN=0"')) + self.assertEqual(code, 2, report) + self.assertIn("overrides required environment", report["error"]) + + def test_test_properties_cannot_override_driver_selection(self): + code, report = self.execute(self.source("base"), self.source("head", property_text='ENVIRONMENT "VK_DRIVER_FILES=/different/driver.json"')) + self.assertEqual(code, 2, report) + self.assertIn("overrides required environment", report["error"]) + + def test_dirty_source_is_rejected_before_output_creation(self): + baseline, candidate = self.source("base"), self.source("head") + (candidate[0] / "untracked.txt").write_text("dirty") + code, report = self.execute(baseline, candidate) + self.assertEqual(code, 2) + self.assertIsNone(report) + + def test_wrong_expected_commit_is_rejected_before_output_creation(self): + baseline, candidate = self.source("base"), self.source("head") + code, report = self.execute(baseline, (candidate[0], "0" * 40)) + self.assertEqual(code, 2) + self.assertIsNone(report) + + def test_existing_output_is_preserved(self): + output = self.root / "evidence" + output.mkdir() + marker = output / "keep" + marker.write_text("existing data") + code, report = self.execute(self.source("base"), self.source("head")) + self.assertEqual(code, 2) + self.assertIsNone(report) + self.assertEqual(marker.read_text(), "existing data") + + def test_empty_selection_is_a_setup_error(self): + code, report = self.execute(self.source("base"), self.source("head"), "--tests", "^missing$") + self.assertEqual(code, 2, report) + self.assertIn("nonempty unique test names", report["error"]) + + def test_unexpected_command_failure_is_retained(self): + code, report = self.execute(self.source("base"), self.source("head", "fail"), "--repeat", "1") + self.assertEqual(code, 1, report) + self.assertTrue(any(entry["returncode"] != 0 for entry in report["iterations"])) + + def test_command_cleanup_removes_descendant_after_parent_exit_and_timeout(self): + for timeout in (True, False): + with self.subTest(timeout=timeout): + marker = self.root / ("timeout-marker" if timeout else "exit-marker") + ready = marker.with_suffix(".ready") + child = f"import time; from pathlib import Path; Path({str(ready)!r}).touch(); time.sleep(1); Path({str(marker)!r}).touch()" + parent = f"import subprocess,sys,time; from pathlib import Path; subprocess.Popen([sys.executable,'-c',{child!r}])\nwhile not Path({str(ready)!r}).exists(): time.sleep(.01)\n" + if timeout: + parent += "time.sleep(30)\n" + with self.assertRaises(subprocess.TimeoutExpired): + gate.run([sys.executable, "-c", parent], self.root / "timeout.log", os.environ, .5) + else: + self.assertEqual(gate.run([sys.executable, "-c", parent], self.root / "exit.log", os.environ, 5), 0) + self.assertTrue(ready.exists()) + time.sleep(1.1) + self.assertFalse(marker.exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/trace_digest.py b/tools/trace_digest.py index b5e21a6fb8..e698b01b3b 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -90,7 +90,11 @@ class Trace: def load_trace(path): opener = gzip.open if str(path).endswith(".gz") else open with opener(path, "rt", encoding="utf-8-sig") as handle: - return Trace(json.load(handle)) + try: + document = json.load(handle) + except RecursionError as error: + raise ValueError(f"{path}: trace JSON nesting is too deep: {error}") from error + return Trace(document) def load_summary_lines(path): diff --git a/tools/tsan-suppressions.txt b/tools/tsan-suppressions.txt new file mode 100644 index 0000000000..6986419b16 --- /dev/null +++ b/tools/tsan-suppressions.txt @@ -0,0 +1,5 @@ +called_from_lib:libvulkan_lvp.so +called_from_lib:libLLVM +called_from_lib:libpulsecommon +race:libLLVM +deadlock:libLLVM diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index ca98d9e02c..49b600fc9b 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -107,6 +107,12 @@ def parse_elf(data): if header[4] != 2 or header[5] != 1: raise Refused("inner ELF is not 64-bit little-endian") program_offset = struct.unpack_from(" tail[0]): tail = (header["offset"], header["source"] - header["offset"]) continue @@ -275,6 +287,28 @@ def unwrap(path, output, verbose=False): "vaddr": header["vaddr"], "align": header["align"]}) regions.sort(key=lambda region: region["old"]) + + def output_region(header): + host = header if header["type"] == PT_LOAD else header["host"] + region = load_regions.get(host["index"]) if host is not None else None + return region if region is not None else region_for(header["source"], header["size"]) + + for header in headers: + if header["size"] == 0 and (header["type"] == PT_LOAD or header["source"] == 0): + continue + region = output_region(header) + alignment = header["align"] + if region is None or alignment <= 1 or alignment & (alignment - 1): + continue + current = region["align"] + current = current if current and (current & (current - 1)) == 0 else 1 + address = header["vaddr"] - (header["source"] - region["old"]) + if (address - region["vaddr"]) % min(current, alignment): + raise Refused("segment %d has incompatible alignment within its output region" % header["index"]) + if alignment > current: + region["align"] = alignment + region["vaddr"] = address + program_offset = struct.unpack_from(" len(result): raise Refused("segment %d escaped the output" % header["index"]) diff --git a/tools/vulkan_validation_gate.py b/tools/vulkan_validation_gate.py new file mode 100644 index 0000000000..d4fa754a81 --- /dev/null +++ b/tools/vulkan_validation_gate.py @@ -0,0 +1,277 @@ +import argparse +import collections +import hashlib +import json +import math +import os +from pathlib import Path +import re +import signal +import subprocess +import sys +import xml.etree.ElementTree as ET + + +FORCED = {"ANYPS5_REQUIRE_VULKAN": "1", "ANYPS5_NO_SHADER_CACHE": "1", + "VK_INSTANCE_LAYERS": "VK_LAYER_KHRONOS_validation"} +PUBLIC_ENV = ("ANYPS5_GPU", "VK_ICD_FILENAMES", "VK_DRIVER_FILES", "VK_LAYER_PATH") +DIAGNOSTIC = re.compile(r"\b(VUID-[A-Za-z0-9_-]+|SYNC-HAZARD-[A-Za-z0-9_-]+)") +MESSAGE = re.compile(r"Validation (?:Error|Warning):|(?:VUID-|SYNC-HAZARD-)[\w-]+\s*\((?:ERROR|WARNING)\s*/") +MISSING_LAYER = "was not found but was requested by env var VK_INSTANCE_LAYERS" + + +class GateError(Exception): + pass + + +def positive(value): + number = float(value) + if not math.isfinite(number) or number <= 0: + raise argparse.ArgumentTypeError("must be finite and positive") + return number + + +def git(source, *arguments): + return subprocess.run(["git", "-C", str(source), *arguments], capture_output=True, + text=True, check=True).stdout.strip() + + +def identity(source, expected): + if not re.fullmatch(r"[0-9a-f]{40}", expected): + raise GateError("expected a full lowercase Git commit ID") + if Path(git(source, "rev-parse", "--show-toplevel")).resolve() != source: + raise GateError(f"{source}: must be a source worktree root") + if git(source, "rev-parse", "HEAD") != expected: + raise GateError(f"{source}: HEAD differs from {expected}") + if git(source, "status", "--porcelain", "--untracked-files=normal"): + raise GateError(f"{source}: source worktree is dirty") + return {"source": str(source), "commit": expected} + + +def run(command, log, environment, timeout): + with log.open("w", encoding="utf-8") as output: + process = subprocess.Popen(command, stdin=subprocess.DEVNULL, stdout=output, stderr=subprocess.STDOUT, + env=environment, start_new_session=True) + try: + return process.wait(timeout=timeout) + finally: + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + process.wait() + + +def cache_values(build): + result = {} + for line in (build / "CMakeCache.txt").read_text(encoding="utf-8-sig").splitlines(): + match = re.match(r"([^#/:][^:]*):[A-Z]+=(.*)", line) + if match: + result[match[1]] = match[2] + return result + + +def artifact_hashes(build, tests, runtime): + paths = set() + for test in tests: + command = test.get("command") or [] + if not command or not Path(command[0]).is_file(): + raise GateError(f"{test['name']}: selected command is not an existing executable") + working = next((Path(prop["value"]) for prop in test.get("properties", []) + if prop["name"] == "WORKING_DIRECTORY"), Path(".")) + if not working.is_absolute(): + working = build / working + for argument in command: + value = argument.partition("=")[2] if argument.startswith("-D") else argument + path = Path(value) + if not path.is_absolute(): + path = working / path + if path.is_file(): + paths.add(path.resolve()) + for relative in runtime: + path = (build / relative).resolve() + if not path.is_relative_to(build) or not path.exists(): + raise GateError(f"runtime path must exist inside the build: {relative}") + if path.is_dir(): + paths.update(p.resolve() for p in path.rglob("*") if p.is_file()) + else: + paths.add(path) + return {str(path): hashlib.sha256(path.read_bytes()).hexdigest() for path in sorted(paths)} + + +def parse_report(path, expected): + root = ET.parse(path).getroot() + if root.tag not in ("testsuite", "testsuites"): + raise GateError(f"{path}: expected JUnit testsuite or testsuites") + results, problems = {}, [] + for case in root.iter("testcase"): + name = case.get("name") + if not name or name in results: + raise GateError(f"{path}: missing or duplicate testcase name") + messages = collections.Counter() + missing = [] + for channel in ("system-out", "system-err"): + for line in (case.findtext(channel) or "").splitlines(): + if MESSAGE.search(line): + messages.update(DIAGNOSTIC.findall(line)) + if MISSING_LAYER in line: + missing.append(line) + failed = (case.find("failure") is not None or case.find("error") is not None + or case.find("skipped") is not None or case.get("status") in ("disabled", "notrun")) + if failed or missing: + problems.append(name) + results[name] = {"diagnostics": dict(messages), "missing_layers": missing, "failed_or_skipped": failed} + if set(results) != set(expected): + problems.append("test coverage differs: missing=" + repr(sorted(set(expected) - set(results))) + + " extra=" + repr(sorted(set(results) - set(expected)))) + return results, problems + + +def compare(iterations): + counts = {name: collections.defaultdict(collections.Counter) for name in ("baseline", "candidate")} + problems = [] + for iteration in iterations: + if iteration["returncode"] or iteration["problems"]: + problems.append({"side": iteration["side"], "iteration": iteration["iteration"], + "returncode": iteration["returncode"], "problems": iteration["problems"]}) + for test, result in iteration["tests"].items(): + counts[iteration["side"]][test].update(result["diagnostics"]) + new = {test: sorted(set(messages) - set(counts["baseline"][test])) + for test, messages in counts["candidate"].items() + if set(messages) - set(counts["baseline"][test])} + return {"counts": {side: {test: dict(messages) for test, messages in tests.items()} + for side, tests in counts.items()}, "new_diagnostics": new, "problems": problems, + "exit_code": int(bool(new or problems))} + + +def save(output, report): + (output / "report.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") + + +def main(argv=None): + parser = argparse.ArgumentParser(description="Build and repeatedly compare focused Vulkan CTests on two pinned clean POSIX worktrees") + parser.add_argument("--baseline", nargs=2, required=True, metavar=("SOURCE", "COMMIT")) + parser.add_argument("--candidate", nargs=2, required=True, metavar=("SOURCE", "COMMIT")) + parser.add_argument("--output", type=Path, required=True, help="new directory outside both source worktrees") + parser.add_argument("--target", action="append", required=True, help="explicit CMake build target; repeat as needed") + parser.add_argument("--tests", required=True, help="CTest regular expression; both builds must select the same nonempty test-name set") + parser.add_argument("--gpu", required=True, help="expected ANYPS5_GPU selection; this is not independent hardware verification") + parser.add_argument("--repeat", type=int, default=2) + parser.add_argument("--timeout", type=positive, default=600, help="outer timeout per configure/build/CTest command") + parser.add_argument("--test-timeout", type=positive, default=30) + parser.add_argument("--cmake-arg", action="append", default=[], help="shared -D cache definition, passed as --cmake-arg=-DNAME=VALUE") + parser.add_argument("--runtime", action="append", default=[], help="build-relative runtime file/directory to hash before and after tests") + parser.add_argument("--cmake", default="cmake") + parser.add_argument("--ctest", default="ctest") + if os.name != "posix": + parser.error("execution is currently supported only on POSIX; Windows process containment is not implemented") + args = parser.parse_args(argv) + if args.repeat <= 0: + parser.error("--repeat must be positive") + if any(not re.fullmatch(r"-D[A-Za-z_][A-Za-z0-9_]*(?::[A-Z]+)?=.*", value) for value in args.cmake_arg): + parser.error("--cmake-arg accepts only -D cache definitions") + report = {"status": "incomplete", "iterations": [], "builds": {}, + "limits": "GPU selection is requested, not independently verified. Only selected command files and explicit --runtime paths are hashed; other dynamic dependencies are not inventoried."} + output = args.output.resolve() + created = False + try: + sources = {side: (Path(values[0]).resolve(), values[1]) + for side, values in (("baseline", args.baseline), ("candidate", args.candidate))} + for source, expected in sources.values(): + identity(source, expected) + if output.is_relative_to(source) or source.is_relative_to(output): + raise GateError("output and source worktrees must be separate trees") + output.mkdir(parents=True, exist_ok=False) + created = True + environment = {**os.environ, **FORCED, "ANYPS5_GPU": args.gpu} + report["environment"] = {key: environment[key] for key in (*FORCED, *PUBLIC_ENV) if key in environment} + report["options"] = {"targets": args.target, "tests": args.tests, "repeat": args.repeat, + "cmake_args": args.cmake_arg, "runtime": args.runtime} + listings, hashes, contexts = {}, {}, {} + for side, (source, expected) in sources.items(): + directory = output / side + directory.mkdir() + build = directory / "build" + commands = [[args.cmake, "-S", str(source), "-B", str(build), "-G", "Ninja", + *args.cmake_arg, "-DCMAKE_BUILD_TYPE=Release", "-DBUILD_TESTING=ON"], + [args.cmake, "--build", str(build), "--target", *args.target]] + record = report["builds"][side] = {**identity(source, expected), "commands": commands} + save(output, report) + for index, command in enumerate(commands): + if run(command, directory / f"build-{index}.log", environment, args.timeout): + raise GateError(f"{side}: configure/build failed; see retained log") + cache = cache_values(build) + if Path(cache["CMAKE_HOME_DIRECTORY"]).resolve() != source: + raise GateError(f"{side}: CMake source attribution differs") + contexts[side] = {key: value for key, value in cache.items() + if key in ("CMAKE_GENERATOR", "CMAKE_BUILD_TYPE") + or re.fullmatch(r"CMAKE_(?:C|CXX)_COMPILER|CMAKE_(?:C|CXX|EXE_LINKER|SHARED_LINKER)_FLAGS(?:_RELEASE)?", key)} + record["configuration"] = contexts[side] + record["compiler_metadata"] = {} + for path in (build / "CMakeFiles").glob("*/CMake*Compiler.cmake"): + metadata = path.read_text(encoding="utf-8") + record["compiler_metadata"][str(path.relative_to(build))] = re.findall( + r'set\((CMAKE_(?:C|CXX)_COMPILER_(?:ID|VERSION)) "([^"]*)"\)', metadata) + contexts[side].update(record["compiler_metadata"][str(path.relative_to(build))]) + listing = directory / "tests.json" + if run([args.ctest, "--test-dir", str(build), "-R", args.tests, "--show-only=json-v1"], + listing, environment, args.timeout): + raise GateError(f"{side}: cannot list selected tests") + tests = json.loads(listing.read_text(encoding="utf-8"))["tests"] + names = [test["name"] for test in tests] + if not names or len(names) != len(set(names)): + raise GateError(f"{side}: expected nonempty unique test names") + for test in tests: + for prop in test.get("properties", []): + if prop["name"] in ("ENVIRONMENT", "ENVIRONMENT_MODIFICATION"): + for setting in prop["value"]: + key, _, value = setting.partition("=") + if key in (*FORCED, *PUBLIC_ENV) and (prop["name"] == "ENVIRONMENT_MODIFICATION" or value != environment.get(key)): + raise GateError(f"{test['name']}: test property overrides required environment {key}") + listings[side] = tests + record["tests"] = tests + hashes[side] = artifact_hashes(build, tests, args.runtime) + record["artifact_sha256"] = hashes[side] + identity(source, expected) + if contexts["baseline"] != contexts["candidate"]: + raise GateError("compiler/build configuration differs between the two builds") + names = {test["name"] for test in listings["baseline"]} + if names != {test["name"] for test in listings["candidate"]}: + raise GateError("selected test-name sets differ between builds") + for iteration in range(1, args.repeat + 1): + for side in (("baseline", "candidate") if iteration % 2 else ("candidate", "baseline")): + directory = output / side + junit = directory / f"iteration-{iteration}.xml" + command = [args.ctest, "--test-dir", str(directory / "build"), "-R", args.tests, "-V", "--parallel", "1", + "--test-output-size-passed", "0", "--test-output-size-failed", "0", + "--timeout", str(args.test_timeout), "--output-junit", str(junit)] + code = run(command, directory / f"iteration-{iteration}.log", environment, args.timeout) + tests, problems = parse_report(junit, names) + report["iterations"].append({"side": side, "iteration": iteration, "command": command, + "returncode": code, "tests": tests, "problems": problems}) + save(output, report) + for side, (source, expected) in sources.items(): + identity(source, expected) + if artifact_hashes(output / side / "build", listings[side], args.runtime) != hashes[side]: + raise GateError(f"{side}: selected command/runtime artifacts changed during testing") + report.update(compare(report["iterations"])) + report["status"] = "complete" + save(output, report) + print(f"{len(names)} tests, {args.repeat} iterations per build; report: {output / 'report.json'}") + return report["exit_code"] + except (GateError, OSError, ValueError, KeyError, ET.ParseError, subprocess.SubprocessError) as error: + report["error"] = str(error) + report["exit_code"] = 2 + if created: + save(output, report) + print(f"validation gate: {error}", file=sys.stderr) + return 2 + except KeyboardInterrupt: + report["exit_code"] = 130 + if created: + save(output, report) + return 130 + + +if __name__ == "__main__": + sys.exit(main())