diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 29891c5c96..0dc28284b0 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -9,6 +9,7 @@ on: - core/relinker/** - demo/web/** - tools/build_web.py + - tools/package_release.py - tools/nid_names.py - tools/progress.py - tools/progress_history.py @@ -42,21 +43,10 @@ jobs: with: ref: main fetch-depth: 0 - - name: Build web converter - shell: bash - run: | - git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$RUNNER_TEMP/emsdk" - "$RUNNER_TEMP/emsdk/emsdk" install 6.0.3 - "$RUNNER_TEMP/emsdk/emsdk" activate 6.0.3 - source "$RUNNER_TEMP/emsdk/emsdk_env.sh" - emcmake cmake -S . -B build-wasm -DANYPS5_BUILD_WASM=ON -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release - cmake --build build-wasm --target relinker_web --parallel 2 - - name: Test web converter - run: python3 tools/test_web.py --build build-wasm - - name: Download latest release libraries + - name: Download matching release converter and libraries env: GITHUB_TOKEN: ${{ github.token }} - run: python3 tools/build_web.py --build build-wasm --libraries + run: python3 tools/build_web.py --build build-wasm --release - run: python3 tools/progress.py build-wasm/web - run: python3 tools/progress_history.py build-wasm/web - uses: actions/upload-pages-artifact@v5 diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index 6d96cce3f8..15a3e8e3dc 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -16,7 +16,7 @@ concurrency: jobs: labels: runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 30 permissions: contents: read pull-requests: write diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7fccd9c6a6..cff3998f8c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,6 +41,7 @@ jobs: steps: - uses: actions/checkout@v7 with: + ref: ${{ github.sha }} submodules: recursive - name: Point the WinLibs CMake at the CA bundle of Git for Windows if: matrix.platform == 'windows' @@ -86,7 +87,7 @@ jobs: run: cmake --build build - name: Run tests (Linux) if: matrix.platform == 'linux' - run: ctest --test-dir build --output-on-failure --timeout 120 --stop-on-failure --no-tests=error + run: ctest --test-dir build --output-on-failure --timeout 120 --stop-on-failure --no-tests=error -E '^precise_wait$' env: ANYPS5_REQUIRE_VULKAN: 1 GALLIUM_OVERRIDE_CPU_CAPS: avx @@ -110,9 +111,38 @@ jobs: if-no-files-found: error compression-level: 0 + web: + name: Build and test web converter + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.sha }} + - name: Build web converter + shell: bash + run: | + git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$RUNNER_TEMP/emsdk" + "$RUNNER_TEMP/emsdk/emsdk" install 6.0.3 + "$RUNNER_TEMP/emsdk/emsdk" activate 6.0.3 + source "$RUNNER_TEMP/emsdk/emsdk_env.sh" + emcmake cmake -S . -B build-wasm -DANYPS5_BUILD_WASM=ON -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release + cmake --build build-wasm --target relinker_web --parallel 2 + - name: Test web converter + run: python3 tools/test_web.py --build build-wasm + - name: Package web converter + shell: bash + run: python3 tools/package_release.py --platform web --build build-wasm --output dist --version "$RELEASE_TAG" --revision "$(git rev-parse HEAD)" + - uses: actions/upload-artifact@v7 + with: + name: release-web + path: dist/* + if-no-files-found: error + compression-level: 0 + publish: name: Publish release - needs: build + needs: [build, web] runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -122,6 +152,8 @@ jobs: GH_REPO: ${{ github.repository }} steps: - uses: actions/checkout@v7 + with: + ref: ${{ github.sha }} - uses: actions/download-artifact@v8 with: pattern: release-* @@ -130,7 +162,7 @@ jobs: - name: Check release files and collect documentation shell: bash run: | - for file in "prx-windows-$RELEASE_TAG.zip" "prx-windows-$RELEASE_TAG.tar.gz" "prx-linux-$RELEASE_TAG.zip" "prx-linux-$RELEASE_TAG.tar.gz" "relinker-$RELEASE_TAG.exe" "relinker-$RELEASE_TAG"; do + for file in "prx-windows-$RELEASE_TAG.zip" "prx-windows-$RELEASE_TAG.tar.gz" "prx-linux-$RELEASE_TAG.zip" "prx-linux-$RELEASE_TAG.tar.gz" "relinker-$RELEASE_TAG.exe" "relinker-$RELEASE_TAG" "web-$RELEASE_TAG.zip"; do test -s "dist/$file" done python tools/package_release.py --docs docs/user --output dist diff --git a/CMakeLists.txt b/CMakeLists.txt index 3334461358..bfd144d963 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,9 @@ if(BUILD_TESTING) get_filename_component(testCompilerDirectory "${CMAKE_CXX_COMPILER}" DIRECTORY) foreach(runtimeName IN ITEMS libgcc_s_seh-1.dll libstdc++-6.dll libwinpthread-1.dll) if(NOT EXISTS "${testCompilerDirectory}/${runtimeName}") + if(ANYPS5_RELINKER_ONLY) + continue() + endif() message(FATAL_ERROR "Cannot locate MinGW runtime: ${runtimeName}") endif() file(COPY "${testCompilerDirectory}/${runtimeName}" DESTINATION "${ANYPS5_TEST_OUTPUT_DIRECTORY}") @@ -167,6 +170,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py) add_test(NAME conventions_ci_gate COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_ci_gate.py) + add_test(NAME conventions_git_errors COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_git.py) add_test(NAME conventions_doc_links COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_doc_links.py) add_test(NAME conventions_unknown_nids COMMAND ${Python3_EXECUTABLE} @@ -184,7 +189,13 @@ if(BUILD_TESTING) add_test(NAME check_main_sync COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_main_sync.py) add_test(NAME configure_seed COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py) + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedTests) + add_test(NAME configure_seed_paths COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedPathTests) + add_test(NAME configure_seed_round_trips COMMAND ${Python3_EXECUTABLE} + ${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 stubgen COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen.py) add_test(NAME debt_split COMMAND ${Python3_EXECUTABLE} @@ -202,10 +213,15 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_wrapped_exports.py) add_test(NAME pr_overlap_own_branch COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_own_branch.py) + add_test(NAME pr_overlap_errors COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_errors.py) add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py) add_test(NAME release_package COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_package_release.py) + add_test(NAME web_release COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_web_release.py) + set_tests_properties(web_release PROPERTIES TIMEOUT 20) add_test(NAME web_library_assets COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_web.py) set_tests_properties(web_library_assets PROPERTIES TIMEOUT 20) diff --git a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp index 13c1d8accc..ed59467ac3 100644 --- a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp +++ b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp @@ -35,6 +35,10 @@ std::optional
ParseHeader(std::span jpeg); std::optional Decode(std::span jpeg); +std::uint32_t ScaledDimension(std::uint32_t dimension, std::uint32_t denominator); + +std::optional DecodeScaled(std::span jpeg, std::uint32_t denominator); + } // namespace Decoder::Jpeg #endif // DECODER_JPEG_HPP diff --git a/core/Decoder/Jpeg/src/Jpeg.cpp b/core/Decoder/Jpeg/src/Jpeg.cpp index d446841cc4..e8e6aa19c3 100644 --- a/core/Decoder/Jpeg/src/Jpeg.cpp +++ b/core/Decoder/Jpeg/src/Jpeg.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #define STB_IMAGE_STATIC @@ -19,8 +20,8 @@ constexpr std::uint32_t MAX_DIMENSION = 0xFFFF; class Handle { public: - Handle() : handle(tj3Init(TJINIT_COMPRESS)) { - if (!handle) throw std::runtime_error("Jpeg::Encode: encoder initialization failed"); + explicit Handle(int type) : handle(tj3Init(type)) { + if (!handle) throw std::runtime_error(type == TJINIT_COMPRESS ? "Jpeg::Encode: encoder initialization failed" : "Jpeg::DecodeScaled: decoder initialization failed"); } ~Handle() { tj3Destroy(handle); } Handle(const Handle&) = delete; @@ -63,7 +64,7 @@ std::vector Encode(std::span pixels, std::uint throw std::invalid_argument("Jpeg::Encode: invalid restart interval"); } const int subsampling = toSubsampling(sampling); - Handle handle; + Handle handle(TJINIT_COMPRESS); if (tj3Set(handle, TJPARAM_QUALITY, quality) < 0 || tj3Set(handle, TJPARAM_SUBSAMP, channels == 1 ? TJSAMP_GRAY : subsampling) < 0) { throw std::runtime_error("Jpeg::Encode: encoder configuration failed"); } @@ -80,6 +81,43 @@ std::vector Encode(std::span pixels, std::uint return {buffer.data, buffer.data + size}; } +std::uint32_t ScaledDimension(std::uint32_t dimension, std::uint32_t denominator) { + if (denominator != 1 && denominator != 2 && denominator != 4 && denominator != 8) { + throw std::invalid_argument("Jpeg::ScaledDimension: the scale must be 1/1, 1/2, 1/4 or 1/8"); + } + const tjscalingfactor factor{1, static_cast(denominator)}; + return static_cast(TJSCALED(static_cast(dimension), factor)); +} + +std::optional DecodeScaled(std::span jpeg, std::uint32_t denominator) { + if (denominator != 1 && denominator != 2 && denominator != 4 && denominator != 8) { + throw std::invalid_argument("Jpeg::DecodeScaled: the scale must be 1/1, 1/2, 1/4 or 1/8"); + } + if (jpeg.empty() || jpeg.size() > INT_MAX) return std::nullopt; + + Handle handle(TJINIT_DECOMPRESS); + if (tj3Set(handle, TJPARAM_STOPONWARNING, 1) < 0) throw std::runtime_error("Jpeg::DecodeScaled: decoder configuration failed"); + if (tj3DecompressHeader(handle, jpeg.data(), jpeg.size()) < 0) return std::nullopt; + const int colorspace = tj3Get(handle, TJPARAM_COLORSPACE); + if (colorspace == TJCS_CMYK || colorspace == TJCS_YCCK) { + throw std::runtime_error("Jpeg::DecodeScaled: scaled decoding of a CMYK or YCCK image is not implemented"); + } + const int width = tj3Get(handle, TJPARAM_JPEGWIDTH); + const int height = tj3Get(handle, TJPARAM_JPEGHEIGHT); + if (width <= 0 || height <= 0) return std::nullopt; + if (tj3SetScalingFactor(handle, tjscalingfactor{1, static_cast(denominator)}) < 0) { + throw std::runtime_error("Jpeg::DecodeScaled: the decoder refused the scaling factor"); + } + + const std::uint32_t channels = colorspace == TJCS_GRAY ? 1 : 3; + Image image{ScaledDimension(static_cast(width), denominator), ScaledDimension(static_cast(height), denominator), channels, {}}; + image.pixels.resize(static_cast(image.width) * image.height * channels); + if (tj3Decompress8(handle, jpeg.data(), jpeg.size(), image.pixels.data(), 0, channels == 1 ? TJPF_GRAY : TJPF_RGB) < 0) { + return std::nullopt; + } + return image; +} + std::optional
ParseHeader(std::span jpeg) { if (jpeg.empty() || jpeg.size() > INT_MAX) return std::nullopt; @@ -97,12 +135,12 @@ std::optional Decode(std::span jpeg) { int width = 0; int height = 0; int channels = 0; - stbi_uc* decoded = stbi_load_from_memory(jpeg.data(), static_cast(jpeg.size()), &width, &height, &channels, 0); + const std::unique_ptr decoded( + stbi_load_from_memory(jpeg.data(), static_cast(jpeg.size()), &width, &height, &channels, 0), stbi_image_free); if (!decoded) return std::nullopt; Image image{static_cast(width), static_cast(height), static_cast(channels), {}}; - image.pixels.assign(decoded, decoded + static_cast(image.width) * image.height * image.channels); - stbi_image_free(decoded); + image.pixels.assign(decoded.get(), decoded.get() + static_cast(image.width) * image.height * image.channels); return image; } diff --git a/core/Decoder/Jpeg/tests/Jpeg.cpp b/core/Decoder/Jpeg/tests/Jpeg.cpp index f5f9c309ab..e19be53028 100644 --- a/core/Decoder/Jpeg/tests/Jpeg.cpp +++ b/core/Decoder/Jpeg/tests/Jpeg.cpp @@ -1,5 +1,7 @@ #include "Decoder/Jpeg.hpp" +#include +#include #include #include #include @@ -108,5 +110,71 @@ int main() { const std::vector garbage{1, 2, 3, 4, 5, 6, 7, 8}; Require(!Decoder::Jpeg::Decode(garbage).has_value()); + Require(Decoder::Jpeg::ScaledDimension(7, 1) == 7 && Decoder::Jpeg::ScaledDimension(7, 2) == 4); + Require(Decoder::Jpeg::ScaledDimension(7, 4) == 2 && Decoder::Jpeg::ScaledDimension(7, 8) == 1); + Require(Decoder::Jpeg::ScaledDimension(32, 8) == 4 && Decoder::Jpeg::ScaledDimension(33, 8) == 5 && Decoder::Jpeg::ScaledDimension(1, 8) == 1); + Require(Decoder::Jpeg::ScaledDimension(0xFFFF, 2) == 0x8000); + for (const std::uint32_t denominator : {0u, 3u, 16u}) { + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::ScaledDimension(7, denominator); })); + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::DecodeScaled(rgbJpeg, denominator); })); + } + + const auto boxError = [](const std::vector& source, std::uint32_t sourceWidth, std::uint32_t sourceHeight, + std::uint32_t channels, const Decoder::Jpeg::Image& scaled, std::uint32_t denominator) { + long error = 0; + for (std::uint32_t y = 0; y < scaled.height; ++y) { + for (std::uint32_t x = 0; x < scaled.width; ++x) { + for (std::uint32_t c = 0; c < channels; ++c) { + long sum = 0; + long count = 0; + for (std::uint32_t sy = y * denominator; sy < std::min(sourceHeight, (y + 1) * denominator); ++sy) { + for (std::uint32_t sx = x * denominator; sx < std::min(sourceWidth, (x + 1) * denominator); ++sx) { + sum += source[(static_cast(sy) * sourceWidth + sx) * channels + c]; + ++count; + } + } + const auto average = static_cast((sum + count / 2) / count); + error += Difference(average, scaled.pixels[(static_cast(y) * scaled.width + x) * channels + c]); + } + } + } + return error / static_cast(static_cast(scaled.width) * scaled.height * channels); + }; + + for (const std::uint32_t denominator : {1u, 2u, 4u, 8u}) { + const auto scaled = Decoder::Jpeg::DecodeScaled(rgbJpeg, denominator); + Require(scaled.has_value() && scaled->channels == 3); + Require(scaled->width == width / denominator && scaled->height == height / denominator); + Require(scaled->pixels.size() == static_cast(scaled->width) * scaled->height * 3); + Require(boxError(rgb, width, height, 3, *scaled, denominator) < 4); + + const auto grayScaled = Decoder::Jpeg::DecodeScaled(grayJpeg, denominator); + Require(grayScaled.has_value() && grayScaled->channels == 1); + Require(grayScaled->width == width / denominator && grayScaled->height == height / denominator); + Require(boxError(gray, width, height, 1, *grayScaled, denominator) < 4); + } + + std::vector odd(31 * 23 * 3); + for (std::uint32_t y = 0; y < 23; ++y) { + for (std::uint32_t x = 0; x < 31; ++x) { + std::uint8_t* pixel = &odd[(y * 31 + x) * 3]; + pixel[0] = static_cast(x * 8); + pixel[1] = static_cast(y * 11); + pixel[2] = static_cast(200 - x * 2); + } + } + const auto oddJpeg = Decoder::Jpeg::Encode(odd, 31, 23, 3, 95, Decoder::Jpeg::Sampling::Yuv420); + for (const std::uint32_t denominator : {2u, 4u, 8u}) { + const auto scaled = Decoder::Jpeg::DecodeScaled(oddJpeg, denominator); + Require(scaled.has_value() && scaled->channels == 3); + Require(scaled->width == Decoder::Jpeg::ScaledDimension(31, denominator) && scaled->height == Decoder::Jpeg::ScaledDimension(23, denominator)); + Require(boxError(odd, 31, 23, 3, *scaled, denominator) < 8); + } + + Require(!Decoder::Jpeg::DecodeScaled({}, 2).has_value()); + Require(!Decoder::Jpeg::DecodeScaled(garbage, 2).has_value()); + const std::vector truncated(rgbJpeg.begin(), rgbJpeg.begin() + static_cast(rgbJpeg.size() / 2)); + Require(!Decoder::Jpeg::DecodeScaled(truncated, 2).has_value()); + return 0; } diff --git a/core/Decoder/Png/src/Png.cpp b/core/Decoder/Png/src/Png.cpp index cb04f25bd5..434a589d6c 100644 --- a/core/Decoder/Png/src/Png.cpp +++ b/core/Decoder/Png/src/Png.cpp @@ -152,12 +152,12 @@ std::optional Decode(std::span png) { int width = 0; int height = 0; int channels = 0; - stbi_uc* decoded = stbi_load_from_memory(png.data(), static_cast(png.size()), &width, &height, &channels, 4); + const std::unique_ptr decoded( + stbi_load_from_memory(png.data(), static_cast(png.size()), &width, &height, &channels, 4), stbi_image_free); if (!decoded) return std::nullopt; Image image{static_cast(width), static_cast(height), {}}; - image.pixels.assign(decoded, decoded + static_cast(image.width) * image.height * 4); - stbi_image_free(decoded); + image.pixels.assign(decoded.get(), decoded.get() + static_cast(image.width) * image.height * 4); return image; } diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 74a5ac5009..a6428b6825 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -354,6 +354,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_move_tests tests/GuestMove.cpp) + target_include_directories(guest_move_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_move_tests PRIVATE libSceMove libc) + configure_windows_unwind(guest_move_tests) + add_test(NAME guest_move COMMAND guest_move_tests) + if(WIN32) + set_tests_properties(guest_move PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp) target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc) @@ -398,6 +408,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_netctl_ap_tests tests/GuestNetCtlAp.cpp) + target_include_directories(guest_netctl_ap_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_netctl_ap_tests PRIVATE libSceNetCtlAp libc) + configure_windows_unwind(guest_netctl_ap_tests) + add_test(NAME guest_netctl_ap COMMAND guest_netctl_ap_tests) + if(WIN32) + set_tests_properties(guest_netctl_ap PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_netctl_ap_dialog_tests tests/GuestNetCtlApDialog.cpp) target_include_directories(guest_netctl_ap_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_netctl_ap_dialog_tests PRIVATE libSceNetCtlApDialog libc) @@ -458,6 +478,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_rudp_poll_tests tests/GuestRudpPoll.cpp) + target_include_directories(guest_rudp_poll_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_rudp_poll_tests PRIVATE libSceRudp libc) + configure_windows_unwind(guest_rudp_poll_tests) + add_test(NAME guest_rudp_poll COMMAND guest_rudp_poll_tests) + if(WIN32) + set_tests_properties(guest_rudp_poll PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_memory_pool_stats_tests tests/GuestMemoryPoolStats.cpp) target_include_directories(guest_memory_pool_stats_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_memory_pool_stats_tests PRIVATE libSceSsl libSceHttp2 libSceNet libkernel libc) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index b18b1ea20f..f371e43165 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1155,8 +1155,8 @@ struct ImeKeycode { std::uint64_t timestamp; }; -using ImeTextFilter = std::int32_t (*)(char16_t* out_text, std::uint32_t* out_text_length, const char16_t* source_text, std::uint32_t source_text_length); -using ImeExtKeyboardFilter = int (*)(const ImeKeycode* source_keycode, std::uint16_t* out_keycode, std::uint32_t* out_status, void* reserved); +using ImeTextFilter = std::int32_t (APS5_VABI *)(char16_t* out_text, std::uint32_t* out_text_length, const char16_t* source_text, std::uint32_t source_text_length); +using ImeExtKeyboardFilter = int (APS5_VABI *)(const ImeKeycode* source_keycode, std::uint16_t* out_keycode, std::uint32_t* out_status, void* reserved); struct ImeExtendedParam { std::uint32_t option; diff --git a/core/libs/prx/libSceAcm/Export.cpp b/core/libs/prx/libSceAcm/Export.cpp index 97dfc19d91..678e096e73 100644 --- a/core/libs/prx/libSceAcm/Export.cpp +++ b/core/libs/prx/libSceAcm/Export.cpp @@ -1,54 +1,84 @@ #include #include +#include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -#include -// Audio co-processor batches complete immediately without producing output; audio is silent. -static constexpr int SCE_ACM_ERROR_INVALID_PARAMETER = static_cast(0x80E30002); -static std::atomic g_nextId{1}; +namespace { + +constexpr int SCE_ACM_ERROR_BAD_CONTEXT = static_cast(0x81940002); +constexpr int SCE_ACM_ERROR_BAD_POINTER = static_cast(0x81940006); + +std::mutex g_mutex; +std::unordered_set g_contexts; +std::atomic g_nextId{1}; + +} extern "C" { int APS5_VABI sceAcmBatchStartBuffer(AcmContextId context, const void* batch_commands, size_t batch_size, AcmBatchError* batch_error, AcmBatchId* batch) { - (void)context; (void)batch_commands; (void)batch_size; - (void)batch_error; - if (!batch) return SCE_ACM_ERROR_INVALID_PARAMETER; - *batch = g_nextId.fetch_add(1, std::memory_order_relaxed); + if (!batch) return SCE_ACM_ERROR_BAD_POINTER; + if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT; + std::lock_guard lock(g_mutex); + if (!g_contexts.contains(context)) return SCE_ACM_ERROR_BAD_CONTEXT; + AcmBatchId id = g_nextId.fetch_add(1, std::memory_order_relaxed); + while (id == 0) id = g_nextId.fetch_add(1, std::memory_order_relaxed); + if (batch_error) std::memset(batch_error, 0, sizeof(*batch_error)); + *batch = id; return 0; } int APS5_VABI sceAcmBatchStartBuffers(AcmContextId context, uint32_t batch_info_count, const AcmBatchInfo* const batch_info[], AcmBatchError* batch_error, AcmBatchId* batch) { - (void)context; (void)batch_info_count; (void)batch_info; - (void)batch_error; - if (!batch) return SCE_ACM_ERROR_INVALID_PARAMETER; - *batch = g_nextId.fetch_add(1, std::memory_order_relaxed); + if (!batch) return SCE_ACM_ERROR_BAD_POINTER; + if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT; + std::lock_guard lock(g_mutex); + if (!g_contexts.contains(context)) return SCE_ACM_ERROR_BAD_CONTEXT; + AcmBatchId id = g_nextId.fetch_add(1, std::memory_order_relaxed); + while (id == 0) id = g_nextId.fetch_add(1, std::memory_order_relaxed); + if (batch_error) std::memset(batch_error, 0, sizeof(*batch_error)); + *batch = id; return 0; } int APS5_VABI sceAcmBatchWait(AcmContextId context, AcmBatchId batch, uint32_t timeout) { - (void)context; (void)batch; (void)timeout; + if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT; + std::lock_guard lock(g_mutex); + if (!g_contexts.contains(context)) return SCE_ACM_ERROR_BAD_CONTEXT; return 0; } int APS5_VABI sceAcmContextCreate(AcmContextId* context) { - if (!context) return SCE_ACM_ERROR_INVALID_PARAMETER; - *context = g_nextId.fetch_add(1, std::memory_order_relaxed); + if (!context) return SCE_ACM_ERROR_BAD_POINTER; + AcmContextId id = g_nextId.fetch_add(1, std::memory_order_relaxed); + while (id == 0) id = g_nextId.fetch_add(1, std::memory_order_relaxed); + { + std::lock_guard lock(g_mutex); + g_contexts.insert(id); + } + *context = id; return 0; } int APS5_VABI sceAcmContextDestroy(AcmContextId context) { - (void)context; + if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT; + std::lock_guard lock(g_mutex); + auto it = g_contexts.find(context); + if (it == g_contexts.end()) return SCE_ACM_ERROR_BAD_CONTEXT; + g_contexts.erase(it); return 0; } -// Builds a convolution-reverb command into a batch; batches execute as no-ops, so nothing is encoded. int APS5_VABI sceAcm_ConvReverb_SharedInput(void) { return 0; } @@ -66,4 +96,14 @@ int APS5_VABI sceAcmBatchJobPriority(void) { return 0; } +int APS5_VABI sceAcm_ConvReverb_SharedIr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAcm_IFFT() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index 351466befa..a411cacdb2 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -428,6 +428,7 @@ configure_windows_unwind(agc_shader_helpers_tests) if(BUILD_TESTING) set_target_properties(agc_shader_helpers_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) add_test(NAME agc_shader_helpers COMMAND agc_shader_helpers_tests) + set_tests_properties(agc_shader_helpers PROPERTIES TIMEOUT 30) if(WIN32) set_tests_properties(agc_shader_helpers PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/prx/libSceAgc/Command/src/Packet.cpp b/core/libs/prx/libSceAgc/Command/src/Packet.cpp index 743fed53f2..6ec0e12165 100644 --- a/core/libs/prx/libSceAgc/Command/src/Packet.cpp +++ b/core/libs/prx/libSceAgc/Command/src/Packet.cpp @@ -1,7 +1,9 @@ #include "prx/libSceAgc/Command/include/Packet.hpp" #include +#include #include +#include #include #include #include @@ -96,8 +98,8 @@ std::uint32_t* WriteRegisterRange(CommandBuffer* buffer, std::uint32_t opcode, s Require(count != 0 && count <= 0x3fffu && offset <= 0xffffu && count <= 0x10000u - offset, function, "invalid register range"); std::vector snapshot; if (values != nullptr) { - CheckAddress(reinterpret_cast(values), 4, function); - snapshot.assign(values, values + count); + snapshot.resize(count); + std::memcpy(snapshot.data(), values, static_cast(count) * sizeof(std::uint32_t)); } auto* packet = Allocate(buffer, count + 2u, function); packet[0] = Header(opcode, count + 2u); diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 336c49b51f..960dbb36cc 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -474,8 +474,14 @@ void testRegisterRange() { check(packet == storage.words.data() + 6 && storage.buffer.cursor_up == storage.words.data() + 12, "incorrect populated register range allocation"); check(storage.words == expected, "register values were not copied correctly"); const auto* misaligned = reinterpret_cast(reinterpret_cast(values.data()) + 1); - expectFailure([&] { sceAgcCbSetShRegisterRangeDirect(&storage.buffer, 0x8c, misaligned, 4); }); - check(storage.words == expected && storage.buffer.cursor_up == storage.words.data() + 12, "misaligned register values modified command buffer"); + packet = sceAgcCbSetShRegisterRangeDirect(&storage.buffer, 0x8c, misaligned, 3); + expected[12] = 0xc0037600u; + expected[13] = 0x8c; + expected[14] = 0x02000000u; + expected[15] = 0x03000000u; + expected[16] = 0x04000000u; + check(packet == storage.words.data() + 12 && storage.buffer.cursor_up == storage.words.data() + 17, "incorrect misaligned register range allocation"); + check(storage.words == expected, "misaligned register values were not read byte for byte"); } void testPacketPayloadAddress() { diff --git a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp index a1e0295daf..5e6be0c709 100644 --- a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp +++ b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp @@ -8,6 +8,8 @@ extern "C" { std::uint32_t* APS5_VABI sceAgcDcbSetCfRegisterDirect(CommandBuffer*, ShaderRegister); std::uint32_t* APS5_VABI sceAgcDcbSetCfRegisterRangeDirect(CommandBuffer*, std::uint32_t, const std::uint32_t*, std::uint32_t); +std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer*, std::uint32_t, const std::uint32_t*, std::uint32_t); +std::uint32_t* APS5_VABI sceAgcCbSetUcRegisterRangeDirect(CommandBuffer*, std::uint32_t, const std::uint32_t*, std::uint32_t); std::uint32_t* APS5_VABI sceAgcDcbSetCxRegisterDirect(CommandBuffer*, ShaderRegister); std::uint32_t* APS5_VABI sceAgcDcbSetShRegisterDirect(CommandBuffer*, ShaderRegister); std::uint32_t* APS5_VABI sceAgcDcbSetUcRegisterDirect(CommandBuffer*, ShaderRegister); @@ -92,11 +94,37 @@ void testConfig() { expectFailure([&] { sceAgcDcbSetCfRegisterDirect(nullptr, {0, 1}); }); expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0x100u, values.data(), 0); }); expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0xffffu, values.data(), 2); }); - const auto* misaligned = reinterpret_cast(reinterpret_cast(values.data()) + 1); - expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0x100u, misaligned, 1); }); check(storage.words == before && storage.buffer.cursor_up == reserved + 4, "invalid config register write modified buffer"); } +void testRangeFromAnyByte() { + const std::array writers{sceAgcCbSetShRegisterRangeDirect, sceAgcCbSetUcRegisterRangeDirect, sceAgcDcbSetCfRegisterRangeDirect}; + const std::array headers{0xc0047600u, 0xc0047900u, 0xc0046800u}; + alignas(16) std::array bytes{}; + for (std::size_t i = 0; i < bytes.size(); ++i) bytes[i] = static_cast(0x10u + i); + struct Case { + std::size_t offset; + std::array values; + }; + const std::array cases{ + Case{1, {0x14131211u, 0x18171615u, 0x1c1b1a19u, 0x201f1e1du}}, + Case{2, {0x15141312u, 0x19181716u, 0x1d1c1b1au, 0x21201f1eu}}, + Case{3, {0x16151413u, 0x1a191817u, 0x1e1d1c1bu, 0x2221201fu}}, + Case{13, {0x201f1e1du, 0x24232221u, 0x28272625u, 0x2c2b2a29u}}, + }; + for (std::size_t i = 0; i < writers.size(); ++i) { + for (const auto& entry : cases) { + Storage storage; + storage.words.fill(0xabcdef01u); + const auto* values = reinterpret_cast(bytes.data() + entry.offset); + auto* packet = writers[i](&storage.buffer, 0x40u, values, 4); + check(packet == storage.words.data() && storage.buffer.cursor_up == packet + 6, "unaligned register range allocation mismatch"); + check(packet[0] == headers[i] && packet[1] == 0x40u && std::equal(entry.values.begin(), entry.values.end(), packet + 2), "unaligned register values were not read byte for byte"); + check(packet[6] == 0xabcdef01u, "unaligned register range overwrote the following word"); + } + } +} + void testIndirect() { const std::array writers{sceAgcDcbSetCxRegistersIndirect, sceAgcDcbSetShRegistersIndirect, sceAgcDcbSetUcRegistersIndirect}; const std::array sizes{sceAgcDcbSetCxRegistersIndirectGetSize, sceAgcDcbSetShRegistersIndirectGetSize, sceAgcDcbSetUcRegistersIndirectGetSize}; @@ -210,6 +238,7 @@ int main() { try { testDirect(); testConfig(); + testRangeFromAnyByte(); testIndirect(); testIndirectPlaceholder(); testDirectList(); diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index adcd1d85a0..1f1ff49e2c 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -266,6 +266,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationPass.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceOverlay.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Screenshot.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Draw.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DepthSurface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTarget.cpp @@ -358,6 +359,7 @@ add_shader_recompiler(shader_recompiler_agc_driver libc target_link_libraries(libSceAgcDriver PRIVATE shader_recompiler_agc_driver + decoder_png SDL2-static libc libkernel @@ -367,6 +369,7 @@ target_link_libraries(libSceAgcDriver PRIVATE if(APS5_ENABLE_TRACY) target_link_libraries(libSceAgcDriver PUBLIC TracyClient) endif() +target_link_options(libSceAgcDriver PRIVATE LINKER:--exclude-libs,$) if(BUILD_TESTING) add_test_executable(agc_driver_draw_program_trace_tests @@ -804,6 +807,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_spirv_failure_text_tests EXCLUDE_FROM_ALL tests/SpirvFailureText.cpp) +target_link_libraries(agc_spirv_failure_text_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_spirv_failure_text_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_spirv_failure_text_tests) +if(BUILD_TESTING) + set_target_properties(agc_spirv_failure_text_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_spirv_failure_text COMMAND agc_spirv_failure_text_tests) + set_tests_properties(agc_spirv_failure_text PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_spirv_failure_text PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_spirv_lds_atomic_lock_tests EXCLUDE_FROM_ALL tests/SpirvLdsAtomicLock.cpp) target_link_libraries(agc_spirv_lds_atomic_lock_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_spirv_lds_atomic_lock_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -817,6 +833,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_spirv_subvector_loop_begin_tests EXCLUDE_FROM_ALL tests/SpirvSubvectorLoopBegin.cpp) +target_link_libraries(agc_spirv_subvector_loop_begin_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_spirv_subvector_loop_begin_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_spirv_subvector_loop_begin_tests) +if(BUILD_TESTING) + set_target_properties(agc_spirv_subvector_loop_begin_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_spirv_subvector_loop_begin COMMAND agc_spirv_subvector_loop_begin_tests) + set_tests_properties(agc_spirv_subvector_loop_begin PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_spirv_subvector_loop_begin PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_layer_input_tests EXCLUDE_FROM_ALL tests/LayerInput.cpp) target_link_libraries(agc_layer_input_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_layer_input_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -1124,6 +1153,12 @@ target_link_libraries(agc_placeholder_images_tests PRIVATE shader_recompiler_agc set_target_properties(agc_placeholder_images_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_placeholder_images_tests) +add_test_executable(agc_screenshot_tests EXCLUDE_FROM_ALL tests/Screenshot.cpp Execution/src/Screenshot.cpp) +target_include_directories(agc_screenshot_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_screenshot_tests PRIVATE decoder_png SDL2-static libc) +set_target_properties(agc_screenshot_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_screenshot_tests) + add_test_executable(agc_shader_device_profile_tests EXCLUDE_FROM_ALL tests/ShaderDeviceProfile.cpp Execution/src/ShaderDeviceProfile.cpp) target_include_directories(agc_shader_device_profile_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_shader_device_profile_tests PRIVATE shader_recompiler_agc_driver libc) @@ -1406,6 +1441,12 @@ if(BUILD_TESTING) endif() add_test(NAME agc_recorder_read_list COMMAND agc_driver_recorder_tests --read-list-only) set_tests_properties(agc_recorder_read_list PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_recorder_deferred_failure_report COMMAND agc_driver_recorder_tests --deferred-failure-report-only) + set_tests_properties(agc_recorder_deferred_failure_report PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_recorder_deferred_failure_report PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_space_pending_writes COMMAND agc_driver_recorder_tests --space-pending-writes-only) set_tests_properties(agc_space_pending_writes PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) @@ -1842,10 +1883,11 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_undithered_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --undithered-alpha-to-coverage-state) + add_test(NAME agc_driver_depth_bounds_state COMMAND agc_driver_graphics_tests --depth-bounds-state) add_test(NAME agc_driver_msaa_dcc_state COMMAND agc_driver_graphics_tests --multisample-dcc-state) add_test(NAME agc_driver_unused_viewport_depth COMMAND agc_driver_graphics_tests --unused-viewport-depth) set_tests_properties(agc_driver_unused_viewport_depth PROPERTIES TIMEOUT 30) - set_tests_properties(agc_driver_undithered_alpha_to_coverage_state PROPERTIES + set_tests_properties(agc_driver_undithered_alpha_to_coverage_state agc_driver_depth_bounds_state PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_driver_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --alpha-to-coverage-state) set_tests_properties(agc_driver_alpha_to_coverage_state PROPERTIES @@ -1863,7 +1905,7 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_dithered_alpha_to_coverage PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_alpha_to_coverage_state agc_driver_undithered_alpha_to_coverage_state agc_driver_msaa_dcc_state agc_driver_unused_viewport_depth PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_alpha_to_coverage_state agc_driver_undithered_alpha_to_coverage_state agc_driver_depth_bounds_state agc_driver_msaa_dcc_state agc_driver_unused_viewport_depth PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") set_tests_properties(agc_driver_dithered_alpha_to_coverage ${multisampledDccClearTests} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -1943,6 +1985,7 @@ target_include_directories(agc_driver_buffer_descriptor_runtime_tests PRIVATE $< target_include_directories(agc_driver_shutdown_threads_tests PRIVATE $) target_include_directories(agc_driver_image_converted_unorm_tests PRIVATE $) +target_include_directories(agc_driver_failed_capture_dump_tests PRIVATE $) target_include_directories(agc_driver_prepared_publication_tests PRIVATE $) @@ -2031,7 +2074,7 @@ if(BUILD_TESTING) 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:$") endif() - foreach(agcTest IN ITEMS agc_vertex_input_tests agc_mesh_output_budget_tests agc_shared_write_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_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_barrier_batch_tests agc_shader_disk_cache_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) + foreach(agcTest IN ITEMS agc_vertex_input_tests agc_mesh_output_budget_tests agc_shared_write_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_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") continue() endif() @@ -2065,6 +2108,7 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_draw_pipeline_trace_disabled PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + set_tests_properties(agc_driver_queued_raw_shader PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=8" TIMEOUT 30) add_test(NAME agc_driver_partially_resident_pending_stores COMMAND agc_driver_partially_resident_snapshot_tests --pending-stores) set_tests_properties(agc_driver_multisample_readback PROPERTIES TIMEOUT 20) foreach(agcReadbackCase IN ITEMS uint8 uint16 uint64 uint128 depth) @@ -2083,6 +2127,9 @@ if(BUILD_TESTING) set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES SKIP_RETURN_CODE 77 RUN_SERIAL TRUE WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 240) if(WIN32) set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + add_test(NAME agc_shader_disk_cache_long_paths COMMAND agc_shader_disk_cache_tests --long-host-paths) + set_tests_properties(agc_shader_disk_cache_long_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() if(WIN32) add_test(NAME agc_driver_untracked_snapshot_hint COMMAND agc_driver_partially_resident_snapshot_tests --untracked-hint) @@ -2125,12 +2172,17 @@ if(BUILD_TESTING) endif() set_tests_properties(agc_sparse_buffer_holes PROPERTIES TIMEOUT 30) add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal) + add_test(NAME agc_pipeline_journal_long_paths COMMAND agc_driver_graphics_tests --pipeline-journal-long-paths) + set_tests_properties(agc_pipeline_journal_long_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_pipeline_library_clear_waits_for_build COMMAND agc_driver_graphics_tests --pipeline-library-clear-waits-for-build) add_test(NAME agc_pipeline_store_lazy_buffer_pool COMMAND agc_driver_graphics_tests --pipeline-store-lazy-buffer-pool) set_tests_properties(agc_pipeline_journal PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_pipeline_library_clear_waits_for_build PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_pipeline_store_lazy_buffer_pool PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) + add_test(NAME agc_pipeline_journal_extended_paths COMMAND agc_driver_graphics_tests --pipeline-journal-extended-paths) + set_tests_properties(agc_pipeline_journal_extended_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_pipeline_journal_long_paths agc_pipeline_journal_extended_paths PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") set_tests_properties(agc_pipeline_journal agc_pipeline_library_clear_waits_for_build agc_pipeline_store_lazy_buffer_pool PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30) @@ -2325,6 +2377,14 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77) add_test(NAME agc_driver_sampled_cache_budget_without_storage_image_multisample COMMAND agc_driver_sampled_cache_budget_tests without-storage-image-multisample) set_tests_properties(agc_driver_sampled_cache_budget_without_storage_image_multisample PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_sampled_cache_device_selection COMMAND agc_driver_sampled_cache_budget_tests device-selection-cpu) + set_tests_properties(agc_sampled_cache_device_selection PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_driver_sampled_cache_device_selection COMMAND agc_driver_sampled_cache_budget_tests device-selection) + set_tests_properties(agc_driver_sampled_cache_device_selection PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_sampled_cache_device_selection agc_driver_sampled_cache_device_selection PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission) add_test(NAME agc_driver_resource_cache_oversized_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-oversized) add_test(NAME agc_driver_resource_cache_storage_clear COMMAND agc_driver_sampled_cache_budget_tests resource-clear) @@ -2549,6 +2609,7 @@ if(AGC_BUILD_VISUAL_TEST) target_link_libraries(agc_driver_visual_test PRIVATE shader_recompiler_agc_driver + decoder_png SDL2-static libc libkernel @@ -3139,6 +3200,6 @@ endif() if(BUILD_TESTING) get_directory_property(agcRegisteredTests TESTS) - list(FILTER agcRegisteredTests EXCLUDE REGEX "^(agc_pipeline_journal|agc_driver_pipeline_journal)$") + list(FILTER agcRegisteredTests EXCLUDE REGEX "^(agc_pipeline_journal(_long_paths|_extended_paths)?|agc_driver_pipeline_journal)$") set_property(TEST ${agcRegisteredTests} APPEND PROPERTY ENVIRONMENT "APS5_NO_PIPELINE_JOURNAL=1") endif() diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp index 25bb3d485a..52582c1143 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp @@ -17,6 +17,7 @@ extern "C" void AgcDriverRaiseWorkerThreadPriority_nid_postfix(const char* role) extern "C" void AgcDriverLockVulkanLoader_nid_postfix(); extern "C" void AgcDriverUnlockVulkanLoader_nid_postfix(); extern "C" void AgcDriverShutdown_nid_postfix(); +extern "C" void AgcDriverRequestScreenshot_nid_postfix(); extern "C" void AgcDriverSuspendPoint_nid_postfix(); extern "C" std::uint64_t AgcDriverDispatchesAdoptedAhead_nid_postfix(); extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp index 5c7fb7d2a6..e683bc7695 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp @@ -10,6 +10,10 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -18,6 +22,7 @@ #include #include #include +#include #include #include @@ -267,6 +272,18 @@ private: Clock::duration folded{}; }; +struct TraceHost { + std::string cpu; + std::uint32_t threads = 0; + std::uint64_t memoryMiB = 0; +}; + +struct TraceTitle { + std::string id; + std::string name; + std::string contentVersion; +}; + struct TraceSummary { std::string commit; std::optional dirty; @@ -281,6 +298,135 @@ struct TraceSummary { double gpuBusyMs = 0; std::string trace; std::optional> interval; + std::optional host; + std::optional title; +}; + +struct TraceBenchInterval { + double startS = 0; + double endS = 0; + std::uint64_t firstPresent = 0; + std::uint32_t width = 0; + std::uint32_t height = 0; + double gpuBusyMs = 0; + std::vector frameUs; +}; + +using TraceBenchIntervals = std::vector>; + +class TraceBench { +public: + static constexpr std::size_t MaximumIntervals = 240; + + static void Keep(TraceBenchIntervals& kept, std::shared_ptr interval, std::uint64_t& dropped) { + kept.push_back(std::move(interval)); + if (kept.size() <= MaximumIntervals) return; + const auto excess = kept.size() - MaximumIntervals; + kept.erase(kept.begin(), kept.begin() + static_cast(excess)); + dropped += excess; + } + + static bool Parse(const char* bench, bool summaryEvery) { + if (bench == nullptr) return false; + if (std::string_view(bench) != "1") throw std::invalid_argument("APS5_TRACE_BENCH=" + std::string(bench) + " is not 1"); + if (!summaryEvery) throw std::invalid_argument("APS5_TRACE_BENCH=1 needs APS5_TRACE_SUMMARY_EVERY"); + return true; + } +}; + +struct TraceBenchSnapshot { + TraceSummary summary; + TraceBenchIntervals intervals; + std::uint64_t dropped = 0; +}; + +class TraceBenchWriter { +public: + explicit TraceBenchWriter(std::function writer) : writer(std::move(writer)) { + if (!this->writer) throw std::invalid_argument("invalid benchmark writer"); + worker = std::thread([this] { run(); }); + } + + TraceBenchWriter(const TraceBenchWriter&) = delete; + TraceBenchWriter& operator=(const TraceBenchWriter&) = delete; + + ~TraceBenchWriter() { + Stop(); + } + + void Stop() { + std::lock_guard stopLock(stopMutex); + { + std::lock_guard lock(mutex); + stopping = true; + } + changed.notify_one(); + if (worker.joinable()) worker.join(); + } + + void Write(TraceBenchSnapshot snapshot) { + { + std::lock_guard lock(mutex); + if (failure) std::rethrow_exception(failure); + if (stopping) throw std::runtime_error("benchmark writer is stopping"); + if (pending) ++superseded; + pending = std::move(snapshot); + } + changed.notify_one(); + } + + void Flush() { + std::unique_lock lock(mutex); + idle.wait(lock, [&] { return failure || (!pending && !writing); }); + if (failure) std::rethrow_exception(failure); + } + + std::uint64_t Superseded() { + std::lock_guard lock(mutex); + return superseded; + } + +private: + void run() noexcept { + try { + for (;;) { + TraceBenchSnapshot snapshot; + { + std::unique_lock lock(mutex); + changed.wait(lock, [&] { return stopping || pending.has_value(); }); + if (!pending) return; + snapshot = std::move(*pending); + pending.reset(); + writing = true; + } + writer(snapshot); + { + std::lock_guard lock(mutex); + writing = false; + } + idle.notify_all(); + } + } catch (...) { + { + std::lock_guard lock(mutex); + failure = std::current_exception(); + writing = false; + } + idle.notify_all(); + } + } + + std::function writer; + std::mutex stopMutex; + std::mutex mutex; + std::condition_variable changed; + std::condition_variable idle; + std::optional pending; + std::uint64_t superseded = 0; + bool writing = false; + bool stopping = false; + std::exception_ptr failure; + std::thread worker; }; struct TraceGpuRange { @@ -429,6 +575,7 @@ public: AppendString(output, summary.driver); output += ",\"os\":"; AppendString(output, summary.os); + AppendMachine(output, summary); output += ",\"resolution\":"; AppendString(output, std::to_string(summary.width) + "x" + std::to_string(summary.height)); output += ",\"window\":{\"spec\":"; @@ -502,7 +649,71 @@ public: return output; } + static std::vector FrameMicroseconds(std::span presents) { + std::vector frames; + for (std::size_t index = 1; index < presents.size(); ++index) { + const auto elapsed = std::chrono::duration_cast(presents[index] - presents[index - 1]).count(); + frames.push_back(static_cast(std::clamp(elapsed, 0, std::numeric_limits::max()))); + } + return frames; + } + + static std::string Bench(const TraceSummary& summary, std::span> intervals, std::uint64_t intervalsDropped) { + std::string output = "{\"format\":\"aps5-bench/1\",\"commit\":"; + AppendString(output, summary.commit); + output += ",\"dirty\":"; + output += summary.dirty.has_value() ? (*summary.dirty ? "true" : "false") : "null"; + output += ",\"env\":{"; + for (std::size_t index = 0; index < summary.environment.size(); ++index) { + if (index != 0) output += ','; + AppendString(output, summary.environment[index].first); + output += ':'; + AppendString(output, summary.environment[index].second); + } + output += "},\"gpu\":"; + AppendString(output, summary.gpu); + output += ",\"driver\":"; + AppendString(output, summary.driver); + output += ",\"os\":"; + AppendString(output, summary.os); + AppendMachine(output, summary); + output += ",\"intervals_dropped\":" + std::to_string(intervalsDropped) + ",\"intervals\":["; + for (std::size_t index = 0; index < intervals.size(); ++index) { + const auto& interval = *intervals[index]; + if (index != 0) output += ','; + output += "\n{\"start_s\":" + number(interval.startS) + ",\"end_s\":" + number(interval.endS); + output += ",\"first_present\":" + std::to_string(interval.firstPresent); + output += ",\"resolution\":"; + AppendString(output, std::to_string(interval.width) + "x" + std::to_string(interval.height)); + output += ",\"gpu_busy_ms\":" + number(interval.gpuBusyMs) + ",\"frame_us\":["; + for (std::size_t frame = 0; frame < interval.frameUs.size(); ++frame) { + if (frame != 0) output += ','; + output += std::to_string(interval.frameUs[frame]); + } + output += "]}"; + } + output += "\n]}\n"; + return output; + } + private: + static void AppendMachine(std::string& output, const TraceSummary& summary) { + if (summary.host) { + output += ",\"cpu\":"; + AppendString(output, summary.host->cpu); + output += ",\"cpu_threads\":" + std::to_string(summary.host->threads); + output += ",\"memory_mib\":" + std::to_string(summary.host->memoryMiB); + } + if (summary.title) { + output += ",\"title_id\":"; + AppendString(output, summary.title->id); + output += ",\"title\":"; + AppendString(output, summary.title->name); + output += ",\"content_version\":"; + AppendString(output, summary.title->contentVersion); + } + } + static double microseconds(Clock::duration elapsed) { return std::chrono::duration(elapsed).count(); } @@ -521,7 +732,7 @@ private: class PerformanceTrace { public: 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; + 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; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp index 617a1399e0..533685ed98 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp @@ -42,7 +42,7 @@ public: { std::lock_guard lock(mutex); if (failure) std::rethrow_exception(failure); - if (stopping) throw std::runtime_error("profile output is stopping"); + if (stopping) return false; if (text.empty()) return true; if (text.size() > capacity - bytes) { ++dropped; @@ -69,6 +69,7 @@ public: } else if (static_cast(size) > capacity) { std::lock_guard lock(mutex); if (failure) std::rethrow_exception(failure); + if (stopping) return; ++dropped; changed.notify_one(); } else { diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Screenshot.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Screenshot.hpp new file mode 100644 index 0000000000..2d8c1c8ec2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Screenshot.hpp @@ -0,0 +1,18 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SCREENSHOT_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SCREENSHOT_HPP + +#include +#include +#include +#include +#include + +namespace AgcDriver::Screenshot { + +void Request(); +bool Take(); +std::filesystem::path Write(std::string_view titleId, std::uint32_t width, std::uint32_t height, std::span bgra); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/TraceFile.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/TraceFile.hpp index 9174bfbc06..ed7bb2a11d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/TraceFile.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/TraceFile.hpp @@ -9,6 +9,7 @@ namespace AgcDriver { class TraceFile { public: static void WriteGzip(const std::filesystem::path& path, std::string_view text); + static void ReplaceGzip(const std::filesystem::path& path, std::string_view text); static void AppendLine(const std::filesystem::path& path, std::string_view line); }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 1b5e098a87..252764922f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -361,6 +361,7 @@ private: // 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 takeScreenshot(bool dumpFrame); void recreateSwapchain(std::uint32_t width, std::uint32_t height, bool force); void refreshSurfaceFormats(); void setHdrMetadata(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp index 72880ca3b3..4009901feb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp @@ -296,8 +296,13 @@ std::filesystem::path ComputePipelineCapture::Write(const std::filesystem::path& CheckCaptureIo(error, "cannot enumerate capture directory"); while (iterator != std::filesystem::directory_iterator{}) { const auto& entry = *iterator; - RequireCaptureCapacity(++count < 128, "capture directory reached 128 entries"); const auto entryStatus = CaptureStatus(entry.path()); + if (std::filesystem::is_regular_file(entryStatus) && entry.path().filename() == ".DS_Store") { + iterator.increment(error); + CheckCaptureIo(error, "cannot advance capture directory"); + continue; + } + RequireCaptureCapacity(++count < 128, "capture directory reached 128 entries"); RequireCapture(std::filesystem::is_directory(entryStatus) && !std::filesystem::is_symlink(entryStatus), "capture directory contains an unexpected entry"); const auto capture = entry.path() / "capture.aps5cp"; const auto captureStatus = CaptureStatus(capture); 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 e55296e46d..b840a0df06 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -359,7 +359,13 @@ void Driver::dispatch(QueueState& queue, std::span packet, totalMs += elapsed; if (profile && elapsed > 200) std::fprintf(stderr, "[gpu] compute shader 0x%llx recompile took %.0f ms (%zu SPIR-V words, %zu captured regions, total %.1f s)\n", static_cast(address), elapsed, compiledResult->spirv.size(), captured.size(), totalMs / 1000); } catch (const std::exception& error) { - const auto dump = dumpShaders ? dumpRequest(address, request) : std::string{}; + std::string dump; + if (dumpShaders) { + const auto regions = shaderMemory->Regions(); + auto failed = request; + failed.context.memory = regions; + dump = dumpRequest(address, failed, "_failed"); + } std::string reason = error.what(); if (const auto newline = reason.find('\n'); newline != std::string::npos) reason.resize(newline); @@ -384,8 +390,8 @@ void Driver::dispatch(QueueState& queue, std::span packet, parked.keepVariant = keepVariant; parked.attachVariant = attachVariant; parked.shaderMemory = shaderMemory; - parked.captured = captured; - parked.liveWords = liveWords; + parked.captured = std::move(captured); + parked.liveWords = std::move(liveWords); parked.capture = capture; parked.dataHit = dataHit; parked.cached = cached; @@ -445,7 +451,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed); const auto queueId = submission.queue; itemCommits.fetch_add(1, std::memory_order_relaxed); - DrawPipeline::Queue0().Enqueue([this, localDevice = std::move(localDevice), result = compiledResult, snapshots = std::move(snapshots), groups, address, queueId, recipe, keepVariant, dataHit, epoch] { + DrawPipeline::Queue0().Enqueue([this, localDevice = std::move(localDevice), result = compiledResult, snapshots = std::move(snapshots), liveWords = std::move(liveWords), groups, address, queueId, recipe, keepVariant, dataHit, epoch] { DrawPipeline::FollowEpoch(epoch); GuestMemory::SetCurrentPacket(0x15, queueId); const auto& compiledCommit = *result; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index 429d4bff59..884514bbb8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -32,12 +33,14 @@ std::shared_ptr Driver::materializeDraw auto& stageCapture = stageCaptures[i]; stageCapture.forgetSerial = GuestMemory::ForgetSerial(); stageCapture.pushOffset = pushOffset; - const auto named = [&](const std::exception& error) { + const auto named = [&](const std::exception& error, std::span regions) { static const bool dumpShaders = std::getenv("APS5_DUMP_SHADERS") != nullptr; std::string dump; if (dumpShaders) { try { - dump = dumpRequest(program.binary.codeAddress, request); + auto failed = request; + failed.context.memory = regions; + dump = dumpRequest(program.binary.codeAddress, failed, "_failed"); } catch (...) {} } std::ostringstream where; @@ -51,7 +54,7 @@ std::shared_ptr Driver::materializeDraw try { return CaptureInvocation(shaderMemory, *program.snapshot, program.codeOffset, request, invocation); } catch (const std::exception& error) { - throw named(error); + throw named(error, shaderMemory.Regions()); } }(); @@ -99,7 +102,7 @@ std::shared_ptr Driver::materializeDraw try { stageCapture.compiled = invocation.Materialize(*capture); } catch (const std::exception& error) { - throw named(error); + throw named(error, request.context.memory); } stageCapture.capture = capture; timing.Mark("materialize"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp index bf6d16754a..1012187b38 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp" #include namespace AgcDriver { @@ -83,6 +84,10 @@ extern "C" void AgcDriverShutdown_nid_postfix() { AgcDriver::Shutdown(); } +extern "C" void AgcDriverRequestScreenshot_nid_postfix() { + AgcDriver::Screenshot::Request(); +} + extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader) try { AgcDriver::RegisterShader(shader); } catch (const ProcessShutdown&) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index b70750163e..55bff4e2ac 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -52,9 +52,9 @@ void PrintFailure(std::exception_ptr error) { std::fflush(stderr); } -constexpr std::uint64_t DriverMemoryAddress = 0xfe0000000ull; +constexpr std::uint64_t DriverMemoryAddress = 0xfe0000000ull + GuestArena::GuestAddressOffset; constexpr std::size_t DriverMemoryBytes = 2u << 20u; -constexpr std::uint64_t FsTableAddress = 0xfe0040000ull; +constexpr std::uint64_t FsTableAddress = DriverMemoryAddress + 0x40000ull; constexpr std::uint64_t FsTableSlotBytes = 16u; constexpr std::uint64_t FsTableSlots = 6u; constexpr std::uint64_t FsRoutineRecords = 8u; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp index 47e17bfdd8..8b0687ac4f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp @@ -67,8 +67,9 @@ std::vector> DispatchWriteRanges(const S bounded = false; continue; } - const auto resource = Graphics::DecodeTextureResource(std::span(binding.guestDescriptor).subspan(static_cast(element) * 8, 8)); - if (resource.baseAddress == 0) continue; + const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * 8, 8); + if (Graphics::IsNullTextureResource(words)) continue; + const auto resource = Graphics::DecodeTextureResource(words); const auto surfaceBytes = Graphics::DescribeSurface(resource).guestBytes; ranges.emplace_back(resource.baseAddress, resource.baseAddress + (surfaceBytes != 0 ? surfaceBytes : UnknownImageBytes)); if (resource.dccAddress != 0) ranges.emplace_back(resource.dccAddress, resource.dccAddress + (surfaceBytes != 0 ? surfaceBytes / 256 + 1 : UnknownImageBytes)); 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 f8214382bf..b110ffd6f1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -2,6 +2,7 @@ #include "ControlFlow/GraphBuilder.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -69,6 +70,11 @@ std::shared_ptr PrepareShaderWithDiagnosti } std::shared_ptr ReadRawShader(std::uint64_t address) { + const auto drain = [](std::uint64_t first, std::size_t bytes) { + if (!DrawPipeline::Active()) return; + auto& pipeline = DrawPipeline::Queue0(); + if (pipeline.Busy() && pipeline.Overlaps(first, bytes)) pipeline.Drain(DrawPipeline::DrainReason::Capture); + }; GuestMemory::CheckRange(reinterpret_cast(address), sizeof(std::uint32_t), 256); static std::mutex cacheMutex; static std::list> cache; @@ -81,6 +87,7 @@ std::shared_ptr ReadRawShader(std::uint64_t address) { } if (cached) { const auto code = std::as_bytes(std::span(cached->code)); + drain(address, code.size()); GuestMemory::FlushGpuWrites(address, code.size()); if (GuestMemory::CompareMapped(address, code) == GuestMemory::Compare::Equal) { std::lock_guard lock(cacheMutex); @@ -101,6 +108,7 @@ std::shared_ptr ReadRawShader(std::uint64_t address) { while (snapshot.code.size() < available) { const auto previous = snapshot.code.size(); snapshot.code.resize(std::min(available, std::max(64, previous * 2))); + drain(address + previous * sizeof(std::uint32_t), (snapshot.code.size() - previous) * sizeof(std::uint32_t)); GuestMemory::Read(address + previous * sizeof(std::uint32_t), std::as_writable_bytes(std::span(snapshot.code).subspan(previous)), alignof(std::uint32_t)); try { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index dc2c695011..36d57da9c9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -494,6 +495,26 @@ void ForgetInaccessiblePages(std::uintptr_t address, std::size_t bytes) { Pages().forgetInaccessible(address, bytes); } +#ifdef __linux__ +int mainImageExtent(dl_phdr_info* info, std::size_t, void* data) { + constexpr auto mask = static_cast(PageBytes - 1); + auto& span = *static_cast(data); + std::uintptr_t first = std::numeric_limits::max(); + std::uintptr_t last = 0; + for (int index = 0; index < info->dlpi_phnum; ++index) { + const auto& header = info->dlpi_phdr[index]; + if (header.p_type != PT_LOAD || header.p_memsz == 0) continue; + first = std::min(first, static_cast(info->dlpi_addr + header.p_vaddr) & ~mask); + last = std::max(last, static_cast(info->dlpi_addr + header.p_vaddr + header.p_memsz + mask) & ~mask); + } + if (first < last) { + span.base = first; + span.size = static_cast(last - first); + } + return 1; +} +#endif + void PageStates::initialize() { if (ready.load(std::memory_order_acquire)) return; std::call_once(once, [&] { @@ -516,6 +537,12 @@ void PageStates::initialize() { image.size = cursor - start; imageCached = image.allocate(); } +#elif defined(__linux__) + static const bool noImageCache = std::getenv("APS5_NO_IMAGE_PAGE_CACHE") != nullptr; + if (!noImageCache) { + dl_iterate_phdr(mainImageExtent, &image); + imageCached = image.allocate(); + } #endif if (arenaCached || imageCached) { GuestAllocations::GuestAllocationsSetInvalidator_nid_postfix(&ForgetPages); @@ -1514,39 +1541,55 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function const auto first = address & ~(page - 1); const auto stop = (address + bytes + page - 1) & ~(page - 1); const bool covered = tracker.covers(first, static_cast(stop - first)); - std::vector saved; - std::vector slots; - if (covered && staysWatched) { - const auto length = static_cast(stop - first); - CheckRange(reinterpret_cast(first), length, 1, false); - const auto* source = reinterpret_cast(first); - slots.resize(length / page); - std::size_t kept = 0; - for (std::size_t index = 0; index < slots.size(); ++index) { - const bool zero = std::memcmp(source + index * page, ZeroPage.data(), page) == 0; - slots[index] = zero ? WindowCopy::Zero : static_cast(kept++); - } - saved.reserve(kept * page); - for (std::size_t index = 0; index < slots.size(); ++index) { - if (slots[index] != WindowCopy::Zero) saved.insert(saved.end(), source + index * page, source + (index + 1) * page); - } - } - const bool before = covered && walkWrites(tracker, first, stop, StampKind::Cpu, true); - bool imported = false; - { + const auto runImport = [&] { #ifdef __APPLE__ const GuestArena::HostWrite open(reinterpret_cast(first), static_cast(stop - first)); #endif - imported = import(); - } - if (!imported) return false; + return import(); + }; if (!staysWatched) { + const bool before = covered && walkWrites(tracker, first, stop, StampKind::Cpu, true); + if (!runImport()) return false; if (!before || !walkWrites(tracker, first, stop, StampKind::ImportWindow, true)) unwatchLocked(tracker, first, static_cast(stop - first)); unwatchLocked(tracker, address, bytes); return true; } + std::vector> spans; + if (covered) spans.emplace_back(first, stop); + else { + for (auto begin = first; begin < stop;) { + const auto end = std::min((begin / WriteBlockBytes + 1) * WriteBlockBytes, stop); + if (tracker.covers(begin, static_cast(end - begin))) { + if (!spans.empty() && spans.back().second == begin) spans.back().second = end; + else spans.emplace_back(begin, end); + } + begin = end; + } + } + std::vector slots(static_cast((stop - first) / page), WindowCopy::Zero); + std::size_t kept = 0; + for (const auto& [begin, end] : spans) { + CheckRange(reinterpret_cast(begin), static_cast(end - begin), 1, false); + for (auto cursor = begin; cursor < end; cursor += page) { + if (std::memcmp(reinterpret_cast(cursor), ZeroPage.data(), page) != 0) slots[static_cast((cursor - first) / page)] = static_cast(kept++); + } + } + std::vector saved; + saved.reserve(kept * page); + const auto* source = reinterpret_cast(first); + for (std::size_t index = 0; index < slots.size(); ++index) { + if (slots[index] != WindowCopy::Zero) saved.insert(saved.end(), source + index * page, source + (index + 1) * page); + } + std::erase_if(spans, [&](const auto& span) { return !walkWrites(tracker, span.first, span.second, StampKind::Cpu, true); }); + if (!runImport()) return false; const WindowCopy window{first, saved, slots}; - if (!before || !walkWrites(tracker, first, stop, StampKind::Cpu, true, &window)) unwatchLocked(tracker, first, static_cast(stop - first)); + auto unwatchedFrom = first; + for (const auto& [begin, end] : spans) { + if (!walkWrites(tracker, begin, end, StampKind::Cpu, true, &window)) continue; + if (unwatchedFrom < begin) unwatchLocked(tracker, unwatchedFrom, static_cast(begin - unwatchedFrom)); + unwatchedFrom = end; + } + if (unwatchedFrom < stop) unwatchLocked(tracker, unwatchedFrom, static_cast(stop - unwatchedFrom)); return true; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp index c913b43155..e9578a2e7c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp @@ -3,11 +3,15 @@ #include "prx/libSceAgcDriver/Execution/include/TraceFile.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/TraceRevision.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" +#include #include +#include #include #include #include #include +#include #ifdef _WIN32 #include #else @@ -15,6 +19,11 @@ #include #include #endif +#if defined(_MSC_VER) && !defined(__clang__) +#include +#elif defined(__x86_64__) || defined(__i386__) +#include +#endif #ifndef _WIN32 extern char** environ; @@ -46,6 +55,9 @@ struct Session { double gpuBusyAtOpen = 0; std::optional gpuClock; std::vector presents; + bool bench = false; + TraceBenchIntervals benchIntervals; + std::uint64_t benchDropped = 0; }; Session& session() { @@ -63,6 +75,11 @@ std::unique_ptr& summaryWriter() { return output; } +std::unique_ptr& benchWriter() { + static std::unique_ptr output; + return output; +} + std::filesystem::path traceDirectory() { const char* directory = std::getenv("APS5_TRACE_DIR"); return std::filesystem::path(directory != nullptr ? directory : "."); @@ -91,6 +108,64 @@ std::string operatingSystem() { #endif } +std::optional processorName() { + std::array words{}; +#if defined(_MSC_VER) && !defined(__clang__) + int registers[4]{}; + __cpuid(registers, static_cast(0x80000000u)); + if (static_cast(registers[0]) < 0x80000004u) return std::nullopt; + for (unsigned leaf = 0; leaf < 3; ++leaf) { + __cpuid(registers, static_cast(0x80000002u + leaf)); + std::memcpy(words.data() + leaf * 4, registers, sizeof registers); + } +#elif defined(__x86_64__) || defined(__i386__) + if (__get_cpuid_max(0x80000000u, nullptr) < 0x80000004u) return std::nullopt; + for (unsigned leaf = 0; leaf < 3; ++leaf) { + unsigned eax = 0, ebx = 0, ecx = 0, edx = 0; + if (__get_cpuid(0x80000002u + leaf, &eax, &ebx, &ecx, &edx) == 0) throw std::runtime_error("cpuid brand string leaf failed"); + words[leaf * 4] = eax; + words[leaf * 4 + 1] = ebx; + words[leaf * 4 + 2] = ecx; + words[leaf * 4 + 3] = edx; + } +#else + return std::nullopt; +#endif + std::string name(reinterpret_cast(words.data()), sizeof words); + name.resize(std::strlen(name.c_str())); + const auto first = name.find_first_not_of(' '); + if (first == std::string::npos) return std::nullopt; + return name.substr(first, name.find_last_not_of(' ') - first + 1); +} + +std::uint64_t memoryMiB() { +#ifdef _WIN32 + MEMORYSTATUSEX status{}; + status.dwLength = sizeof(status); + if (::GlobalMemoryStatusEx(&status) == 0) throw std::system_error(static_cast(::GetLastError()), std::system_category(), "GlobalMemoryStatusEx"); + return status.ullTotalPhys / (1024 * 1024); +#else + const long pages = ::sysconf(_SC_PHYS_PAGES); + const long size = ::sysconf(_SC_PAGE_SIZE); + if (pages <= 0 || size <= 0) throw std::system_error(errno, std::generic_category(), "sysconf physical memory"); + return static_cast(pages) * static_cast(size) / (1024 * 1024); +#endif +} + +const std::optional& host() { + static const auto value = []() -> std::optional { + auto cpu = processorName(); + if (!cpu) return std::nullopt; + return TraceHost{std::move(*cpu), std::thread::hardware_concurrency(), memoryMiB()}; + }(); + return value; +} + +std::optional title() { + if (!HasAppParamJson_nid_no_patch()) return std::nullopt; + return TraceTitle{GetAppTitleId_nid_postfix().value, GetAppTitle_nid_postfix().value, GetAppContentVersion_nid_no_patch().value}; +} + std::vector> environment() { #ifdef _WIN32 char** entries = _environ; @@ -120,6 +195,9 @@ TraceSummary identity(std::uint32_t width, std::uint32_t height, const VulkanDev summary.os = operatingSystem(); summary.width = width; summary.height = height; + summary.host = host(); + static const auto running = title(); + summary.title = running; return summary; } @@ -185,6 +263,23 @@ void PerformanceTrace::FlushInterval(Clock::time_point now, std::uint32_t width, summary.gpuBusyMs = VulkanDevice::PresentCounts().gpuBusyMs - state.intervalGpuBusy; summary.interval = std::make_pair(seconds(state.intervalPresents.front() - state.first), seconds(now - state.first)); summaryWriter()->Write(TraceFormat::Summary(summary, snapshots, state.intervalPresents)); + if (!state.bench) return; + TraceBench::Keep(state.benchIntervals, std::make_shared(TraceBenchInterval{summary.interval->first, summary.interval->second, summary.firstPresent, width, height, summary.gpuBusyMs, TraceFormat::FrameMicroseconds(state.intervalPresents)}), state.benchDropped); + if (!benchWriter()) { + const auto name = "aps5-bench-" + summary.commit + "-" + std::to_string(processId()) + ".json.gz"; + benchWriter() = std::make_unique([directory = traceDirectory(), name](const TraceBenchSnapshot& snapshot) { + try { + std::filesystem::create_directories(directory); + TraceFile::ReplaceGzip(directory / name, TraceFormat::Bench(snapshot.summary, snapshot.intervals, snapshot.dropped)); + } catch (const std::exception& error) { + std::fprintf(stderr, "[trace] writing %s failed: %s\n", name.c_str(), error.what()); + std::fflush(stderr); + throw; + } + }); + ProfilePrint_nid_no_patch("[trace] benchmark frames go to %s, rewritten every interval\n", name.c_str()); + } + benchWriter()->Write(TraceBenchSnapshot{std::move(summary), state.benchIntervals, state.benchDropped}); } void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const VulkanDevice* device) { @@ -195,6 +290,7 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const if (!state.started) { state.window = TraceWindow::Parse(std::getenv("APS5_TRACE_FRAMES"), std::getenv("APS5_TRACE_SECONDS")); state.interval = TraceInterval::Parse(std::getenv("APS5_TRACE_SUMMARY_EVERY")); + state.bench = TraceBench::Parse(std::getenv("APS5_TRACE_BENCH"), state.interval.has_value()); state.first = now; state.started = true; } @@ -287,6 +383,7 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const void PerformanceTrace::StopOutput() { if (writer()) writer()->Stop(); if (summaryWriter()) summaryWriter()->Stop(); + if (benchWriter()) benchWriter()->Stop(); } } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp new file mode 100644 index 0000000000..6a930d4e30 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp @@ -0,0 +1,77 @@ +#include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp" +#include "Decoder/Png.hpp" +#include "SDL_filesystem.h" +#include "SDL_error.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +std::atomic requested{false}; + +std::filesystem::path directory() { + const char* configured = std::getenv("ANYPS5_SCREENSHOTS"); + if (configured != nullptr && configured[0] != '\0') return configured; + const std::unique_ptr basePath(SDL_GetBasePath(), SDL_free); + if (!basePath) throw std::runtime_error(std::string("Screenshot: cannot locate the executable: ") + SDL_GetError()); + return std::filesystem::path(reinterpret_cast(basePath.get())) / "anyps5-screenshots"; +} + +std::string localTimestamp() { + const std::time_t now = std::time(nullptr); + std::tm local{}; +#ifdef _WIN32 + if (localtime_s(&local, &now) != 0) throw std::runtime_error("Screenshot: cannot read the local time"); +#else + if (localtime_r(&now, &local) == nullptr) throw std::runtime_error("Screenshot: cannot read the local time"); +#endif + char text[32]; + if (std::strftime(text, sizeof(text), "%Y%m%d-%H%M%S", &local) == 0) throw std::runtime_error("Screenshot: cannot format the local time"); + return text; +} + +std::filesystem::path unusedPath(const std::filesystem::path& folder, const std::string& stem) { + for (int index = 0; index < 1000; ++index) { + const auto path = folder / (index == 0 ? stem + ".png" : stem + "-" + std::to_string(index) + ".png"); + if (!std::filesystem::exists(path)) return path; + } + throw std::runtime_error("Screenshot: no unused file name for " + stem + " in " + folder.string()); +} + +} + +void AgcDriver::Screenshot::Request() { + requested.store(true); +} + +bool AgcDriver::Screenshot::Take() { + return requested.exchange(false); +} + +std::filesystem::path AgcDriver::Screenshot::Write(std::string_view titleId, std::uint32_t width, std::uint32_t height, std::span bgra) { + if (titleId.empty()) throw std::invalid_argument("Screenshot: empty title ID"); + const auto pixelCount = static_cast(width) * height; + if (width == 0 || height == 0 || bgra.size() != pixelCount * 4) throw std::invalid_argument("Screenshot: invalid BGRA8 frame"); + std::vector rgb(pixelCount * 3); + for (std::size_t pixel = 0; pixel < pixelCount; ++pixel) { + rgb[pixel * 3] = std::to_integer(bgra[pixel * 4 + 2]); + rgb[pixel * 3 + 1] = std::to_integer(bgra[pixel * 4 + 1]); + rgb[pixel * 3 + 2] = std::to_integer(bgra[pixel * 4]); + } + const auto png = Decoder::Png::Encode(rgb, width, height, 3); + const auto folder = directory(); + std::filesystem::create_directories(folder); + const auto path = unusedPath(folder, std::string(titleId) + "-" + localTimestamp()); + std::ofstream file; + file.exceptions(std::ios::failbit | std::ios::badbit); + file.open(path, std::ios::binary); + file.write(reinterpret_cast(png.data()), static_cast(png.size())); + file.close(); + return path; +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp index 40d0a7e089..b8263c881e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp @@ -6,7 +6,11 @@ #include #include #include +#include #include +#ifdef _WIN32 +#include +#endif namespace AgcDriver { @@ -50,6 +54,22 @@ void TraceFile::WriteGzip(const std::filesystem::path& path, std::string_view te output.close(); } +void TraceFile::ReplaceGzip(const std::filesystem::path& path, std::string_view text) { + auto temporary = path; + temporary += ".tmp"; + WriteGzip(temporary, text); + std::error_code error; +#ifdef _WIN32 + if (::MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) == 0) error = std::error_code(static_cast(::GetLastError()), std::system_category()); +#else + std::filesystem::rename(temporary, path, error); +#endif + if (!error) return; + std::error_code removeError; + std::filesystem::remove(temporary, removeError); + throw std::system_error(error, "replacing " + path.string()); +} + void TraceFile::AppendLine(const std::filesystem::path& path, std::string_view line) { std::ofstream output; output.exceptions(std::ios::badbit | std::ios::failbit); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index aedf2f6afb..f63c30b6bc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -24,6 +24,8 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" +#include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" @@ -155,24 +157,30 @@ public: if (worker.joinable()) worker.join(); } - void Enqueue(int index, std::uint32_t width, std::uint32_t height, std::uint32_t scale, std::vector pixels) { + void Enqueue(int index, std::uint32_t width, std::uint32_t height, std::uint32_t scale, bool screenshot, std::vector pixels) { std::lock_guard lock(mutex); require(!stopping, "frame dump writer has stopped"); require(pixels.size() <= maxBytes - queuedBytes, "frame dump queue exceeded 64 MiB"); - if (!frameDumpOutput.Enabled_nid_no_patch()) return; + if (!screenshot && !frameDumpOutput.Enabled_nid_no_patch()) return; if (!worker.joinable()) worker = std::thread([this] { run(); }); const auto bytes = pixels.size(); - pending.push_back({index, width, height, scale, std::move(pixels)}); + pending.push_back({index, width, height, scale, screenshot, std::move(pixels)}); queuedBytes += bytes; changed.notify_one(); } + bool Idle() { + std::lock_guard lock(mutex); + return queuedBytes == 0; + } + private: struct Frame { int index; std::uint32_t width; std::uint32_t height; std::uint32_t scale; + bool screenshot; std::vector pixels; }; @@ -187,7 +195,8 @@ private: batch.swap(pending); } for (const auto& frame : batch) { - WriteFrameBmp(frame.index, frame.width, frame.height, frame.pixels, frame.scale); + if (frame.screenshot) std::fprintf(stderr, "[gpu] screenshot saved: %s\n", Screenshot::Write(GetAppTitleId_nid_postfix().value, frame.width, frame.height, frame.pixels).string().c_str()); + else WriteFrameBmp(frame.index, frame.width, frame.height, frame.pixels, frame.scale); std::lock_guard lock(mutex); queuedBytes -= frame.pixels.size(); } @@ -449,6 +458,7 @@ struct VulkanDevice::State { std::uint32_t dumpHeight = 0; std::uint32_t dumpScale = 1; bool dumpRecorded = false; + bool dumpScreenshot = false; int dumpIndex = 0; std::uint32_t imageIndex = 0; bool inFlight = false; @@ -480,6 +490,7 @@ struct VulkanDevice::State { bool queuePending = false; std::uint32_t queueIndex = 0; int nextDumpIndex = 0; + bool nextDumpScreenshot = false; FrameDumpWriter dumpWriter; template @@ -678,6 +689,11 @@ struct VulkanDevice::State { require(slot.dumpBuffer != nullptr, "frame dump was not recorded"); const auto full = slot.dumpBuffer->Bytes(); const auto index = slot.dumpIndex; + if (slot.dumpScreenshot) { + require(full.size() == static_cast(slot.dumpWidth) * slot.dumpHeight * 4, "invalid screenshot pixels"); + dumpWriter.Enqueue(index, slot.dumpWidth, slot.dumpHeight, 1, true, std::vector(full.begin(), full.end())); + return; + } const auto scale = slot.dumpScale; require(scale != 0 && full.size() == static_cast(slot.dumpWidth) * slot.dumpHeight * 4, "invalid frame dump pixels"); const auto width = (slot.dumpWidth + scale - 1) / scale; @@ -688,7 +704,7 @@ struct VulkanDevice::State { } static const auto start = std::chrono::steady_clock::now(); std::fprintf(stderr, "[gpu] frame_%03d.bmp: %ux%u display read back on the GPU at %.1f s\n", index, slot.dumpWidth, slot.dumpHeight, std::chrono::duration(std::chrono::steady_clock::now() - start).count()); - dumpWriter.Enqueue(index, width, height, 1, std::move(pixels)); + dumpWriter.Enqueue(index, width, height, 1, false, std::move(pixels)); } // Blocks on the fence, or polls it every 50 us with APS5_PRESENT_POLL_FENCE=1 (the difference @@ -1485,7 +1501,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : VkPhysicalDeviceExtendedDynamicStateFeaturesEXT dynamicStateFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT}; VkPhysicalDeviceDynamicRenderingFeaturesKHR renderingFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES_KHR}; constexpr std::array libraryExtensions{VK_KHR_PIPELINE_LIBRARY_EXTENSION_NAME, VK_EXT_GRAPHICS_PIPELINE_LIBRARY_EXTENSION_NAME, VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME, VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME, VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME, VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME}; - if (std::getenv("APS5_NO_GPL") == nullptr && std::all_of(libraryExtensions.begin(), libraryExtensions.end(), hasExtension)) { + bool libraryDevice = false; + if (std::all_of(libraryExtensions.begin(), libraryExtensions.end(), hasExtension)) { dynamicStateFeatures.pNext = &renderingFeatures; libraryFeatures.pNext = &dynamicStateFeatures; VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &libraryFeatures}; @@ -1493,8 +1510,9 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : VkPhysicalDeviceGraphicsPipelineLibraryPropertiesEXT libraryProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_PROPERTIES_EXT}; VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &libraryProperties}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); - state->graphicsPipelineLibrary = libraryFeatures.graphicsPipelineLibrary == VK_TRUE && dynamicStateFeatures.extendedDynamicState == VK_TRUE && renderingFeatures.dynamicRendering == VK_TRUE && libraryProperties.graphicsPipelineLibraryFastLinking == VK_TRUE && properties.properties.limits.maxPushConstantsSize >= Graphics::PipelinePushConstantBytes; - if (state->graphicsPipelineLibrary) deviceExtensions.insert(deviceExtensions.end(), libraryExtensions.begin(), libraryExtensions.end()); + libraryDevice = libraryFeatures.graphicsPipelineLibrary == VK_TRUE && dynamicStateFeatures.extendedDynamicState == VK_TRUE && renderingFeatures.dynamicRendering == VK_TRUE && libraryProperties.graphicsPipelineLibraryFastLinking == VK_TRUE && properties.properties.limits.maxPushConstantsSize >= Graphics::PipelinePushConstantBytes; + if (libraryDevice) deviceExtensions.insert(deviceExtensions.end(), libraryExtensions.begin(), libraryExtensions.end()); + state->graphicsPipelineLibrary = libraryDevice && std::getenv("APS5_NO_GPL") == nullptr; if (state->graphicsPipelineLibrary && hasExtension(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME)) { VkPhysicalDeviceDriverProperties driver{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES}; VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver}; @@ -1720,7 +1738,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : depthClipFeatures.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &depthClipFeatures; } - if (state->graphicsPipelineLibrary) { + if (libraryDevice) { renderingFeatures.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &libraryFeatures; } @@ -3060,6 +3078,13 @@ std::optional> CompressedClearPixel(const DisplayBuffer return DisplayBufferClearPixel(buffer, keys); } +std::vector DisplayPixels(const DisplayBuffer& buffer, const std::optional>& cleared) { + if (!cleared) return ReadDisplayBuffer(buffer); + std::vector pixels(static_cast(buffer.width) * buffer.height * cleared->size()); + for (std::size_t offset = 0; offset < pixels.size(); offset += cleared->size()) std::memcpy(pixels.data() + offset, cleared->data(), cleared->size()); + return pixels; +} + // Debug aid: APS5_NO_RESIDENT_PRESENT=1 always presents through guest memory. bool NoResidentPresent() { static const bool no = std::getenv("APS5_NO_RESIDENT_PRESENT") != nullptr; @@ -3111,10 +3136,14 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { dumpFrame = true; } } - if (!present(buffer.width, buffer.height, true, pixels, nullptr, nullptr, VK_FILTER_LINEAR, dumpFrame, false, nullptr, 0, passthrough)) { + 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 (dumpFrame) --dumps.dumped; + if (screenshot) Screenshot::Request(); return false; } + if (!shot.empty()) state->dumpWriter.Enqueue(0, buffer.width, buffer.height, 1, true, std::move(shot)); return true; } static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; @@ -3160,9 +3189,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { bool dumpFrame = false; if (frameDumpOutput.Enabled_nid_no_patch() && dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { if (dumps.cpu || (passthrough && resident == nullptr)) { - auto full = cleared ? std::vector(static_cast(buffer.width) * buffer.height * cleared->size()) : ReadDisplayBuffer(buffer); - for (std::size_t offset = 0; cleared && offset < full.size(); offset += cleared->size()) std::memcpy(full.data() + offset, cleared->data(), cleared->size()); - WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, full, DumpScale()); + WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, DisplayPixels(buffer, cleared), DumpScale()); } else { state->nextDumpIndex = dumps.dumped++; dumpFrame = true; @@ -3173,6 +3200,9 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { lastReport = std::chrono::steady_clock::now(); AgcDriver::ProfilePrint_nid_no_patch("[flip] (cumulative): %llu presents from the resident image (%llu refreshed first), through guest memory: %llu not pending, %llu unsuitable; %llu GPU frame dumps\n", static_cast(residentPresents), static_cast(refreshedPresents), static_cast(notPending), static_cast(unsuitable), static_cast(gpuDumps)); } + const bool screenshot = takeScreenshot(dumpFrame); + const bool cpuScreenshot = screenshot && passthrough && resident == nullptr; + auto shot = cpuScreenshot ? DisplayPixels(buffer, cleared) : std::vector{}; CaptureTrace::Log("present dump=%d address=%llx width=%u height=%u resident=%d generation=%llu", dumpFrame ? state->nextDumpIndex : -1, static_cast(buffer.address), buffer.width, buffer.height, resident != nullptr, static_cast(resident ? resident->Generation() : 0)); if (passthrough && resident == nullptr) ensureHdrScaler(DisplayTexelFormat(buffer.pixelFormat)); VkClearColorValue uniform{}; @@ -3182,14 +3212,21 @@ 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, 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)) { // A dropped frame (swapchain out of date) keeps the dump numbering contiguous. if (dumpFrame) --dumps.dumped; + if (screenshot) Screenshot::Request(); return false; } + if (cpuScreenshot) state->dumpWriter.Enqueue(0, buffer.width, buffer.height, 1, true, std::move(shot)); return true; } +bool VulkanDevice::takeScreenshot(bool dumpFrame) { + state->nextDumpScreenshot = !dumpFrame && state->dumpWriter.Idle() && Screenshot::Take(); + return state->nextDumpScreenshot; +} + bool VulkanDevice::AcquireImage() { PerformanceTimer timing("Vulkan.Acquire"); require(state->swapchain != VK_NULL_HANDLE, "device has no swapchain"); @@ -3363,7 +3400,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu else scaler->RecordBlit(commands, barrier.image, state->extent.width, state->extent.height); } if (dumpFrame) { - const auto scale = DumpScale(); + const auto scale = state->nextDumpScreenshot ? 1 : DumpScale(); if (drawn || direct) { const auto extent = PresentationPass::DumpExtent(width, height, scale); slot.dumpWidth = extent.width; @@ -3431,6 +3468,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu slot.shared = shared; // Only a submitted readback is written after the fence (a failed submit tears the device down). slot.dumpRecorded = dumpFrame; + slot.dumpScreenshot = dumpFrame && state->nextDumpScreenshot; slot.dumpIndex = state->nextDumpIndex; slot.recorderSerial = batchesAtBlit; slot.previousSerial = state->lastPresentSerial; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 4c13b4bea4..c73dcc10bb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -79,6 +79,11 @@ struct DescriptorRejected : std::runtime_error { using std::runtime_error::runtime_error; }; +struct BdaUnalignedAccess : std::runtime_error { + BdaUnalignedAccess(const std::string& message, std::uint32_t instruction) : std::runtime_error(message), instruction(instruction) {} + std::uint32_t instruction; +}; + // Device entry points resolved once per device (VulkanDevice's State fills it after setup): the // loader's vkGetDeviceProcAddr is a name lookup under its global mutex per call, paid at every // record site otherwise. Null members (tests, APS5_NO_PROC_TABLE=1) resolve per call (Resolved). diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 4448b60a54..ceec04cf3e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -246,6 +246,7 @@ struct MirrorStats { std::uint64_t sparseBinds = 0; }; MirrorStats MirrorCounters(); +std::uint64_t SnapshotMismatches(); void ClearSnapshotCopies(VkDevice device); std::vector AddressTableKey(std::span> writes, std::span own); void ClearImageMirrors(VkDevice device); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index f4d473d7a5..dc217a1d52 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -196,6 +196,7 @@ public: std::optional DescribePendingWrite(std::uint64_t address, std::size_t bytes) const; // Ends and submits the open batch without waiting. void Submit(); + void HoldNextSubmit(VkSemaphore semaphore, std::uint64_t value); // Submits the open batch and returns the serial of the newest submitted batch (0 when nothing was // ever submitted); WaitSerial(serial) then covers all recorded work. std::uint64_t SubmitAndEpoch(); @@ -804,6 +805,8 @@ private: Context context; VkSemaphore timeline = VK_NULL_HANDLE; + VkSemaphore submitHold = VK_NULL_HANDLE; + std::uint64_t submitHoldValue = 0; // Identity for a thread that released the mutex around a wait (syncThroughUnlocked): the // recorder may have been torn down meanwhile, so it is looked up by this id, not by pointer. std::uint64_t id = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 7b4aaa4858..5569cd738a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -42,6 +42,7 @@ struct TextureCacheUse { std::uint64_t storageBytes = 0; }; TextureCacheUse TextureCacheUsage(); +void OverrideCacheBudgetForTest(std::uint64_t bytes); std::uint64_t SampledTextureCacheBudget(const Context& context); std::shared_ptr CachedSampledTexture(const Context& context, std::span words, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D); bool SampledTexturesShareEntry(std::span first, std::span second); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp index dbff513454..5699bf78e1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp @@ -75,8 +75,8 @@ struct SurfaceGeometry { }; SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor); bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t levels); -bool TracksBlockUnits(const GuestTextureResource& surface, const SurfaceGeometry& geometry); GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed); +GuestTextureResource ArrayLayerWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index e6365c0aca..6e2537248b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -244,10 +244,10 @@ void BdaResources::CheckFault() const { ShaderRecompiler::BdaAbi::Fault report{}; std::memcpy(&report, fault->Bytes().data(), sizeof(report)); if (report.state == ShaderRecompiler::BdaAbi::FaultState::Empty) { - Require(static_cast(report.reason) == 0 && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.reserved == 0, "BDA fault record has data without publication"); + Require(static_cast(report.reason) == 0 && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.otherFaults == 0, "BDA fault record has data without publication"); return; } - Require(report.state == ShaderRecompiler::BdaAbi::FaultState::Ready && report.reserved == 0, "incomplete or invalid BDA fault record"); + Require(report.state == ShaderRecompiler::BdaAbi::FaultState::Ready && report.otherFaults <= 1, "incomplete or invalid BDA fault record"); Require(report.reason != ShaderRecompiler::BdaAbi::FaultReason::InvalidRectangle, "rect-list requires finite nondegenerate axis-aligned positions with equal positive W"); if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::LoopLimit) { // APS5_LOOP_GUARD: the shader left a loop that ran past the guard; the dispatch result is kept. @@ -293,6 +293,10 @@ void BdaResources::CheckFault() const { break; } } + if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::Unaligned) { + if (report.otherFaults == 0) throw BdaUnalignedAccess(message.str(), report.instruction); + message << "; a fault of another kind was recorded in the same dispatch"; + } throw std::runtime_error(message.str()); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index bce39d0a09..acc5cde4bb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2103,6 +2103,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& Require(!draw.indirect && !state.primitiveRestart && !state.rectList && !state.quadList && !state.stages.mesh && !state.stages.tessellation && state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, "stencil replay supports direct triangle lists without restart, mesh or tessellation"); Require(!state.depthBiasPerFace && !ShadersWriteMemory(shaders), "stencil replay refuses face splits and shader memory writes"); Require(std::ranges::all_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->stencilReplaySafe && (shader.stage == ShaderRecompiler::ShaderStage::Vertex || shader.stage == ShaderRecompiler::ShaderStage::Fragment); }), "stencil replay refuses stages with side effects, clocks, subgroup dependencies or primitive ID inputs"); + if (stencilReferences.size() == 256u) { + static std::once_flag unbounded; + std::call_once(unbounded, [] { APS5_LOG_CHARS_OUT("Stencil replay: a draw replays all 256 stencil reference values because its pixel shader's export is not bounded, recording 256 passes per triangle and instance"); }); + } for (const auto& shader : shaders) { if (shader.stage != ShaderRecompiler::ShaderStage::Vertex) continue; Require((shader.program->hostSubgroupSize == 1u || shader.program->hostSubgroupSize == context.subgroup.subgroupSize) && shader.program->spirv.size() >= 5u && shader.program->spirv[1] >= 0x00010000u && shader.program->spirv[1] < 0x00010600u, "stencil replay vertex subgroup contract differs from the physical device"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 6549e853b5..56368f852f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -2840,6 +2840,7 @@ LeaseState& Leases() { struct SnapshotStats { std::atomic checked{0}; std::atomic skipped{0}; + std::atomic mismatched{0}; }; SnapshotStats& Snapshots() { @@ -3072,6 +3073,10 @@ MirrorStats MirrorCounters() { return stats; } +std::uint64_t SnapshotMismatches() { + return Snapshots().mismatched.load(std::memory_order_relaxed); +} + std::vector AddressTableKey(std::span> writes, std::span own) { std::vector key; key.reserve(writes.size() * 2u + 1u + own.size() * 4u); @@ -4124,10 +4129,9 @@ void GuestBufferMemory::AddSnapshot(const GuestMemorySnapshot& snapshot) { static const bool fatal = std::getenv("APS5_SNAPSHOT_CHECK_FATAL") != nullptr; if (!live || fatal) throw std::runtime_error(text); static std::mutex reportMutex; - static std::uint64_t mismatches = 0; static auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60); std::lock_guard lock(reportMutex); - ++mismatches; + const auto mismatches = Snapshots().mismatched.fetch_add(1, std::memory_order_relaxed) + 1; const auto now = std::chrono::steady_clock::now(); if (now - lastReport >= std::chrono::seconds(10)) { lastReport = now; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index c07a32acc8..3c81c9c97e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -203,8 +203,8 @@ GuestTextureResource DecodeTextureResource(std::span words) Require(width == height, "guest cube texture descriptor is not square"); Require(baseArray <= depth, "guest cube texture descriptor has a base array past its last array slice"); if (baseArray == depth) depth += 5u; - if ((depth - baseArray + 1u) % 6u != 0) { - Require(false, "guest cube texture descriptor does not contain a multiple of 6 array slices" + + if (depth - baseArray + 1u < 6u) { + Require(false, "guest cube texture descriptor holds fewer than 6 array slices" + describe() + " (base array " + std::to_string(baseArray) + ")"); } break; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 11bcccdaf3..f0829abb3c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -36,10 +36,22 @@ std::uint32_t MultisampledBlockLog2(ColorTileMode mode) { } } +std::uint64_t collectBlocks(std::uint64_t address, std::size_t bytes) { + constexpr std::uint64_t block = 65536; + const auto first = address & ~(block - 1); + const auto stop = (address + bytes + block - 1) & ~(block - 1); + const auto generation = GuestMemory::CollectWritesUncached(first, static_cast(stop - first)); + return generation != 0 ? generation : GuestMemory::CollectWritesUncached(address, bytes); +} + bool sameTarget(const ColorTarget& a, const ColorTarget& b) { return a.surfaceAddress == b.surfaceAddress && a.format == b.format && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.samples == b.samples; } +bool recreated(const ColorTarget& resident, const ColorTarget& use) { + return use.dccAddress != 0 && use.pipeBankXor != resident.pipeBankXor && use.address == resident.address && use.dccAddress == resident.dccAddress && use.dccControl == resident.dccControl && use.dccAlphaOnMsb == resident.dccAlphaOnMsb && use.dccPipeAligned == resident.dccPipeAligned && use.cmaskAddress == resident.cmaskAddress && use.cmaskBytes == resident.cmaskBytes && use.tileMode == resident.tileMode && use.elementBytes == resident.elementBytes; +} + VkClearColorValue fastClearColor(const ColorTarget& target) { Require(target.elementBytes != 0 && target.elementBytes <= sizeof(target.clearWords) && 16u % target.elementBytes == 0, "the fast clear of a multisampled target over 64-bit texels is not modeled"); std::array texels{}; @@ -215,6 +227,24 @@ public: }); } + void Recreate(const ColorTarget& use) { + SettleStorageWrites(); + target = use; + cmask = MultisampledCmask(use.cmaskAddress, use.cmaskBytes); + colorFill = MultisampledColorFill(use); + fromTexture = false; + dccRanges.clear(); + metadataValid = std::make_shared>(true); + const auto initial = initialDccClear(use); + recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &initial, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + }); + } + void SettleStorageWrites() { if (!storageWritten) return; storageWritten = false; @@ -365,7 +395,7 @@ private: std::vector metadata(dccRanges.begin() + 1, dccRanges.end()); for (auto& range : metadata) { if (!GuestMemory::Watched(range.address, range.bytes)) return false; - range.generation = GuestMemory::CollectWritesUncached(range.address, range.bytes); + 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})); @@ -386,7 +416,7 @@ private: for (auto& range : dccRanges) { GuestMemory::CheckRange(reinterpret_cast(range.address), range.bytes, 256); Require(GuestMemory::Watched(range.address, range.bytes), "initial multisampled DCC contents require watched guest memory"); - range.generation = GuestMemory::CollectWritesUncached(range.address, range.bytes); + range.generation = collectBlocks(range.address, range.bytes); Require(range.generation != 0, "initial multisampled DCC contents have no write generation"); } for (std::size_t first = 0; first < dccRanges.size(); ++first) { @@ -529,6 +559,17 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { for (const auto& target : targets()) { if (!target->active->load(std::memory_order_acquire)) continue; if (target->context.device != context.device || !sameTarget(target->target, use)) continue; + if (recreated(target->target, use)) { + { + std::unique_lock lock(guardedMutex()); + std::erase_if(guarded(), [&](const auto& range) { return range.active == target->active; }); + anyGuarded().store(!guarded().empty(), std::memory_order_release); + } + target->Recreate(use); + guard(context, use, target->active, target->metadataValid); + target->ApplyFastClear(use); + return *target; + } if (target->fromTexture && use.cmaskAddress != 0) { target->fromTexture = false; target->cmask = MultisampledCmask(use.cmaskAddress, use.cmaskBytes); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 465fa91e9e..eaca4be3f4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -224,6 +224,11 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& } } } + if (resources.split) { + const auto unused = 3u & ~(keys.preRasterizationSets | keys.fragmentSets); + keys.preRasterizationSets |= unused & 1u; + keys.fragmentSets |= unused & 2u; + } for (const auto& binding : input.bindings) { appendKey(keys.vertexInput, binding.binding); appendKey(keys.vertexInput, binding.stride); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp index abfda3fae7..4b09eaf041 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp" #include "ShaderCacheDirectory.hpp" +#include "ShaderDiskCache.hpp" #include #include #include @@ -45,9 +46,11 @@ PipelineCache::PipelineCache(const Context& context, const VkPhysicalDevicePrope std::snprintf(digits, sizeof(digits), "%02x", static_cast(byte)); uuid += digits; } + char version[17]; + std::snprintf(version, sizeof(version), "%016llx", static_cast(ShaderRecompiler::ShaderDiskCache::SourceVersion())); char name[160]; std::snprintf(name, sizeof(name), "vk-pipelines-%04x-%04x-%08x-%s.bin", properties.vendorID, properties.deviceID, properties.driverVersion, uuid.c_str()); - path = directory / name; + path = directory / version / name; } std::vector initialData; if (!path.empty()) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 45481c3c5f..c7a0f679a5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -564,8 +564,15 @@ private: Journal() { const char* off = std::getenv("APS5_NO_PIPELINE_JOURNAL"); if ((off != nullptr && std::strcmp(off, "0") != 0) || !ShaderRecompiler::ShaderDiskCache::Enabled()) return; - path = ShaderRecompiler::ShaderDiskCache::EntryDirectory() / "pipelines.journal"; - lockPath = ShaderRecompiler::ShaderDiskCache::EntryDirectory() / "pipelines.journal.lock"; + auto directory = ShaderRecompiler::ShaderDiskCache::EntryDirectory(); +#ifdef _WIN32 + std::error_code error; + const auto native = ShaderRecompiler::NativeCachePath_nid_no_patch(directory, error); + if (error) throw std::filesystem::filesystem_error("cannot resolve the pipeline journal directory", directory, error); + directory = native; +#endif + path = directory / "pipelines.journal"; + lockPath = directory / "pipelines.journal.lock"; maxBytes = configured("APS5_PIPELINE_JOURNAL_MAX_MIB", 1024u) << 20u; std::thread([this] { run(); }).detach(); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 6655a45e82..d646322de2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include "prx/libc/include/general/AtomicSharedPtr.hpp" namespace AgcDriver::Graphics { @@ -3396,6 +3397,11 @@ bool Recorder::writtenBackSince(std::uint64_t sequence, std::uint64_t begin, std return false; } +void Recorder::HoldNextSubmit(VkSemaphore semaphore, std::uint64_t value) { + submitHold = semaphore; + submitHoldValue = value; +} + void Recorder::Submit() { PerformanceTimer frameTiming("Recorder.Submit"); // The work count is cleared even when nothing is open: the driver counts a dispatch after its @@ -3445,13 +3451,23 @@ void Recorder::Submit() { const std::uint64_t serial = submissions + 1; CaptureTrace::Log("submit batch=%llu reads=%zu writes=%zu", static_cast(serial), batch->reads.size(), batch->writes.size()); VkTimelineSemaphoreSubmitInfoKHR timelineInfo{VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO_KHR}; - timelineInfo.signalSemaphoreValueCount = 1; - timelineInfo.pSignalSemaphoreValues = &serial; + const VkSemaphore hold = std::exchange(submitHold, VK_NULL_HANDLE); + const std::uint64_t holdValue = submitHoldValue; + VkPipelineStageFlags holdStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + if (hold != VK_NULL_HANDLE) { + submission.waitSemaphoreCount = 1; + submission.pWaitSemaphores = &hold; + submission.pWaitDstStageMask = &holdStage; + timelineInfo.waitSemaphoreValueCount = 1; + timelineInfo.pWaitSemaphoreValues = &holdValue; + } if (timeline != VK_NULL_HANDLE) { - submission.pNext = &timelineInfo; + timelineInfo.signalSemaphoreValueCount = 1; + timelineInfo.pSignalSemaphoreValues = &serial; submission.signalSemaphoreCount = 1; submission.pSignalSemaphores = &timeline; } + if (hold != VK_NULL_HANDLE || timeline != VK_NULL_HANDLE) submission.pNext = &timelineInfo; 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) { @@ -3954,6 +3970,16 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source, bool for (auto& action : batch->completions) { try { action(); + } catch (const BdaUnalignedAccess& error) { + static std::mutex failuresMutex; + static std::unordered_map failures; + constexpr std::uint64_t kDeferredFailureFirst = 8, kDeferredFailureEvery = 1024; + const auto count = [&] { + std::lock_guard lock(failuresMutex); + return ++failures[error.instruction]; + }(); + if (count <= kDeferredFailureFirst) std::fprintf(stderr, "[gpu] deferred write-back failed: %s\n", error.what()); + else if (count % kDeferredFailureEvery == 0) std::fprintf(stderr, "[gpu] deferred write-back failed with an unaligned BDA access at pc 0x%x %llu times so far; latest: %s\n", error.instruction, static_cast(count), error.what()); } catch (const std::exception& error) { AgcDriver::LogCoalescedStderr_nid_no_patch("[gpu] deferred write-back failed: %s\n", error.what()); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index ad59adeada..f1055a3663 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" @@ -274,16 +275,17 @@ bool admitTexture(TextureCache& cache, const TextureKey& key, std::uint64_t inco cache.firstUses.clear(); cache.firstUsesPresent = present; } - const bool firstUse = incoming != 0 && present != 0 && !cache.firstUses.contains(key); + const bool scan = incoming != 0 && present != 0; + const bool firstUse = scan && !cache.firstUses.contains(key); const auto ceiling = budget + budget / 4u; while (!cache.entries.empty() && cache.bytes + incoming > budget) { const auto back = std::prev(cache.entries.end()); - if (firstUse && back->present + 1u >= present) { + const bool over = cache.bytes + incoming > ceiling; + if (scan && !over && (firstUse ? back->present + 1u >= present : back->present == present)) { cache.firstUses.insert(key); ++cache.counts.uncached; return false; } - const bool over = cache.bytes + incoming > ceiling; const bool keep = back->accounted == 0 || (!over && back->texture.use_count() > 1); if (keep && cache.passed < cache.entries.size()) { cache.entries.splice(cache.entries.begin(), cache.entries, back); @@ -306,7 +308,7 @@ void reportCacheCounts(TextureCache& cache, std::uint64_t budget) { if (now - cache.countsReported < std::chrono::seconds(10)) return; cache.countsReported = now; const auto counts = std::exchange(cache.counts, {}); - AgcDriver::ProfilePrint_nid_no_patch("[texture-cache] (10 s): %llu textures cached, %llu used uncached (first use this frame, the least recently used entry used this frame or the last), %llu evicted, %llu held ones passed over, %llu found changed; %llu MiB in %zu entries, budget %llu MiB, frame %llu\n", static_cast(counts.inserted), static_cast(counts.uncached), static_cast(counts.evicted), static_cast(counts.passed), static_cast(counts.stale), static_cast(cache.bytes >> 20u), cache.entries.size(), static_cast(budget >> 20u), static_cast(Recorder::Presents())); + AgcDriver::ProfilePrint_nid_no_patch("[texture-cache] (10 s): %llu textures cached, %llu used uncached (first use this frame with the least recently used entry used this frame or the last, or a later use with it used this frame), %llu evicted, %llu held ones passed over, %llu found changed; %llu MiB in %zu entries, budget %llu MiB, frame %llu\n", static_cast(counts.inserted), static_cast(counts.uncached), static_cast(counts.evicted), static_cast(counts.passed), static_cast(counts.stale), static_cast(cache.bytes >> 20u), cache.entries.size(), static_cast(budget >> 20u), static_cast(Recorder::Presents())); } struct ImageMemoEntry { @@ -551,7 +553,10 @@ std::optional largestDeviceLocalHeap(const VkPhysicalDeviceMemory return heap; } +std::atomic budgetOverride{0}; + std::uint64_t forcedCacheBudget() { + if (const auto overridden = budgetOverride.load(std::memory_order_relaxed); overridden != 0) return overridden; static const std::uint64_t forced = [] { const char* text = std::getenv("APS5_TEXTURE_CACHE_MIB"); return text != nullptr ? std::strtoull(text, nullptr, 10) << 20u : 0u; @@ -658,7 +663,9 @@ void reportGuestAllocation(const GuestTextureResource& resource, std::uint64_t g } // `guestBytes` is the surface size when the caller described the surface already (0: described here). -std::shared_ptr cachedTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D) { +std::shared_ptr cachedTexture(const Context& context, std::span words, const GuestTextureResource& requested, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D) { + const auto resource = ArrayLayerWindow(requested, context.limits.maxImageArrayLayers); + if (resource.baseAddress != requested.baseAddress) guestBytes = 0; if (resource.dimension == TextureDimension::k2DMsaa) { Require(!depthCompare, "native multisampled depth comparison sampling is not implemented"); if (auto depth = DepthSurfaceTexture(context, words, resource, components, multisampleViewType)) { @@ -1095,7 +1102,9 @@ static WrittenStorage WrittenStorageBytes(const GuestTextureResource& viewed, st } // `guestBytes` is the surface size when the caller described the surface already (0: described here). -std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& viewed, std::uint32_t mip, std::uint64_t guestBytes, bool written) { +std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& requested, std::uint32_t mip, std::uint64_t guestBytes, bool written) { + 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; @@ -1220,6 +1229,10 @@ std::uint64_t StorageTextureCacheBudget(const Context& context) { return storageBudget(context); } +void OverrideCacheBudgetForTest(std::uint64_t bytes) { + budgetOverride.store(bytes, std::memory_order_relaxed); +} + TextureCacheUse TextureCacheUsage() { TextureCacheUse use; { @@ -5477,13 +5490,20 @@ bool SkipWriteBack() { } void ShaderResources::WriteBack() { - WriteBackBuffers(); - if (SkipWriteBack()) return; - for (std::size_t index = 0; index < storageTextures.size(); ++index) { - if (!storageWritten[index] || storageTextures[index] == nullptr) continue; - storageTextures[index]->MarkDirty(); - static_cast(DepthSurfaceStoreBack(context, *storageTextures[index])); + std::exception_ptr unaligned; + try { + WriteBackBuffers(); + } catch (const BdaUnalignedAccess&) { + unaligned = std::current_exception(); } + if (!SkipWriteBack()) { + for (std::size_t index = 0; index < storageTextures.size(); ++index) { + if (!storageWritten[index] || storageTextures[index] == nullptr) continue; + storageTextures[index]->MarkDirty(); + static_cast(DepthSurfaceStoreBack(context, *storageTextures[index])); + } + } + if (unaligned) std::rethrow_exception(unaligned); } void ShaderResources::MarkGpuWrites(Recorder& recorder) { @@ -5529,9 +5549,16 @@ void ShaderResources::CheckFaults() const { } void ShaderResources::WriteBackBuffers() { - if (bda) bda->CheckFault(); - if (SkipWriteBack()) return; - guestMemory.WriteBack(); + std::exception_ptr unaligned; + if (bda) { + try { + bda->CheckFault(); + } catch (const BdaUnalignedAccess&) { + unaligned = std::current_exception(); + } + } + if (!SkipWriteBack()) guestMemory.WriteBack(); + if (unaligned) std::rethrow_exception(unaligned); } bool ShaderResources::ReadsOverlap(std::uint64_t address, std::size_t bytes) const { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 2b8d12e603..40f39f6a74 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -710,7 +710,7 @@ std::set ValidateShaders(std::span shaders, const auto color = std::find_if(state.colors.begin(), state.colors.end(), [&](const ColorTarget& target) { return target.exportIndex == location; }); const bool uintExport = color != state.colors.end() && color->uintExport; const bool packed = color != state.colors.end() && color->packing != ShaderRecompiler::ColorExportPacking::None; - Require(signature == (uintExport || packed ? "vertex:u32x4" : "vertex:f32x4"), packed ? "fragment shader must export uint4 words to its packed color attachments" : uintExport ? "fragment shader must export uint4 colors to its unsigned integer attachments" : "fragment shader must export float4 colors to its attachments"); + Require(color == state.colors.end() || signature == (uintExport || packed ? "vertex:u32x4" : "vertex:f32x4"), packed ? "fragment shader must export uint4 words to its packed color attachments" : uintExport ? "fragment shader must export uint4 colors to its unsigned integer attachments" : "fragment shader must export float4 colors to its attachments"); locations.insert(location); } Require((!state.dualSourceBlend && state.reversedBlend == 0) || previous.secondSource == "f32x4", "dual-source blending needs a float4 second color at location 0 index 1"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index a6f0c3829a..090a68abc1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -53,6 +53,12 @@ float readFloat(const Registers& registers, std::uint32_t offset) { return value; } +float readDepthBound(const Registers& registers, std::uint32_t offset) { + const auto value = std::bit_cast(read(registers, offset)); + if (std::isnan(value)) Require(false, "NaN depth bound at DWORD " + std::to_string(offset)); + return value; +} + std::string zeroMessage(std::uint32_t offset, std::uint32_t value, const char* name) { char detail[64]; std::snprintf(detail, sizeof(detail), " (register 0x%x = 0x%08x)", offset, value); @@ -361,8 +367,8 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx result.depthBoundsTest = (depthControl & 8u) != 0; if (result.depthBoundsTest) { Require(zFormat != 0, "depth bounds without a depth plane"); - result.minDepthBounds = readFloat(cx, 0x008); - result.maxDepthBounds = readFloat(cx, 0x009); + result.minDepthBounds = readDepthBound(cx, 0x008); + result.maxDepthBounds = readDepthBound(cx, 0x009); } if (depthClear) { result.depthTest = true; @@ -1337,9 +1343,8 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } else { const auto screenBottomRight = read(cx, 0xd); APS5_LOG_OUT_DEBUG("No color target, screen BR register=0x%x", screenBottomRight); - result.renderExtent = {screenBottomRight & 0xffffu, screenBottomRight >> 16u}; + result.renderExtent = {std::max(screenBottomRight & 0xffffu, 1u), std::max(screenBottomRight >> 16u, 1u)}; APS5_LOG_OUT_DEBUG("Render extent from screen=%ux%u", result.renderExtent.width, result.renderExtent.height); - Require(result.renderExtent.width != 0 && result.renderExtent.height != 0, "empty framebuffer extent for a draw without color writes"); } result.quadSampleMask = decodeQuadSampleMask(cx, result.samples); result.pixelEarlyZ = !PixelProgramSkipped(queue) && PixelEarlyZ(read(cx, 0x203)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 1c4afad236..a2d90b6926 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -335,6 +335,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex imageInfo.format = vkFormat; imageInfo.extent = {descriptor.width, descriptor.height, geometry.imageDepth}; imageInfo.mipLevels = descriptor.mipCount; + Require(geometry.imageLayers <= context.limits.maxImageArrayLayers, "a texture array with more layers than maxImageArrayLayers is not implemented"); imageInfo.arrayLayers = geometry.imageLayers; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; @@ -525,7 +526,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; if (descriptor.dimension == TextureDimension::kCube) { - Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; @@ -632,7 +633,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; if (descriptor.dimension == TextureDimension::kCube) { - Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; @@ -747,7 +748,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; if (descriptor.dimension == TextureDimension::kCube) { - Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; @@ -1014,7 +1015,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; const auto viewLayerCount = std::min(geometry.imageLayers, source->ImageLayers()) - descriptor.baseArray; if (descriptor.dimension == TextureDimension::kCube) { - Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = source->Image(); @@ -1373,7 +1374,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, const bool layered = geometry.layers >= 2 && !geometry.thick && geometry.imageDepth == 1 && geometry.layers == geometry.imageLayers && geometry.layerBytes * geometry.layers == guestBytes && geometry.layerBytes % 65536 == 0 && descriptor.baseAddress % 65536 == 0; // Tile blocks divide the stamp blocks, so at an aligned base every unit is whole tile blocks // of its (layer, mip) slices (sliceWindows). - blockUnits = LayerTrackingEnabled() && BlockTrackingEnabled() && TracksBlockUnits(descriptor, geometry); + blockUnits = LayerTrackingEnabled() && BlockTrackingEnabled() && !geometry.thick && descriptor.tileMode != TextureTileMode::kLinear && descriptor.baseAddress % 65536 == 0; trackedLayers = blockUnits ? static_cast((guestBytes + 65535) / 65536) : layered && LayerTrackingEnabled() ? geometry.layers : 1u; trackedLayerBytes = blockUnits ? 65536 : guestBytes / trackedLayers; layerGeneration.assign(trackedLayers, 0); @@ -1390,6 +1391,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, imageInfo.format = vkFormat; imageInfo.extent = {descriptor.width, descriptor.height, geometry.imageDepth}; imageInfo.mipLevels = descriptor.mipCount; + Require(geometry.imageLayers <= context.limits.maxImageArrayLayers, "a texture array with more layers than maxImageArrayLayers is not implemented"); imageInfo.arrayLayers = geometry.imageLayers; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index 56233ce130..ff1bed0787 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -466,10 +466,6 @@ bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t leve return true; } -bool TracksBlockUnits(const GuestTextureResource& surface, const SurfaceGeometry& geometry) { - return !geometry.thick && surface.tileMode != TextureTileMode::kLinear && surface.baseAddress % 65536 == 0 && (surface.dccAddress == 0 || surface.mipCount == 1); -} - GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed) { const bool layered = viewed.dimension == TextureDimension::k2DArray || viewed.dimension == TextureDimension::k1DArray || viewed.dimension == TextureDimension::kCube; if (!layered || viewed.baseArray == 0 || viewed.baseArray > viewed.depthOrLastArray || viewed.dccAddress != 0) return viewed; @@ -490,6 +486,25 @@ GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed) { return window; } +GuestTextureResource ArrayLayerWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers) { + const bool layered = viewed.dimension == TextureDimension::k2DArray || viewed.dimension == TextureDimension::k1DArray || viewed.dimension == TextureDimension::kCube; + if (viewed.depthOrLastArray < maxArrayLayers || !layered) return viewed; + Require(viewed.dimension != TextureDimension::kCube, "an image with more layers than maxImageArrayLayers that is not a 1D or 2D array is not implemented"); + Require(viewed.baseArray <= viewed.depthOrLastArray, "an image view's first array slice lies past its last"); + Require(viewed.depthOrLastArray - viewed.baseArray < maxArrayLayers, "an image view of more array slices than maxImageArrayLayers is not implemented"); + if (const auto xorMode = XorSwizzleMode(viewed.tileMode); xorMode != 0u) { + const auto* equation = FindTextureSwizzleEquation(xorMode, BytesPerElement(viewed.format)); + Require(equation != nullptr && std::none_of(equation->bits.begin(), equation->bits.end(), [](std::uint32_t bit) { return (bit & 0xff000000u) != 0; }), "an image view past maxImageArrayLayers of a swizzle that depends on the slice is not implemented"); + } + const auto geometry = DescribeSurface(viewed); + Require(!geometry.thick && geometry.layers == viewed.depthOrLastArray + 1u, "an array past maxImageArrayLayers must be a thin array"); + auto window = viewed; + window.baseAddress = viewed.baseAddress + geometry.GuestLayerOffset(viewed.baseArray); + window.depthOrLastArray = viewed.depthOrLastArray - viewed.baseArray; + window.baseArray = 0; + return window; +} + std::vector ComputeElementMipLayout(TextureTileMode tileMode, std::uint32_t bytesPerElement, std::uint32_t width, std::uint32_t height, std::uint32_t mipCount) { Require(width != 0 && height != 0 && mipCount != 0 && mipCount <= 16u, "invalid surface mip chain"); if (tileMode == TextureTileMode::kLinear) return ComputeLinearMipLayout(bytesPerElement, 1u, 1u, width, height, mipCount); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp index 4fb1f76a56..eddc4438b9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp @@ -518,6 +518,20 @@ void RunBdaExecutionTests(const Context& context) { } ranges[0].permissions = 0; run(guest, 8, 0, Abi::FaultReason::Permission); + const auto sequence = [&](std::initializer_list reasons, Abi::FaultReason recorded, std::uint32_t otherFaults) { + const Abi::Header header{Abi::Version, 2, sizeof(Abi::Range), 0}; + std::memcpy(table.Bytes().data(), &header, sizeof(header)); + std::memcpy(table.Bytes().data() + sizeof(header), ranges.data(), sizeof(ranges)); + std::memset(fault.Bytes().data(), 0, fault.Bytes().size()); + Pipeline pipeline(context, MakeBdaFaultSequenceTestShader(reasons), {&table, &fault, &output}); + pipeline.Run(1); + Abi::Fault report{}; + std::memcpy(&report, fault.Bytes().data(), sizeof(report)); + Require(report.state == Abi::FaultState::Ready && report.reason == recorded && report.otherFaults == otherFaults, "BDA faults of one dispatch were not recorded with the first reason and the sticky other-fault flag"); + }; + sequence({Abi::FaultReason::Unaligned, Abi::FaultReason::Unaligned}, Abi::FaultReason::Unaligned, 0); + sequence({Abi::FaultReason::Unaligned, Abi::FaultReason::Unmapped}, Abi::FaultReason::Unaligned, 1); + sequence({Abi::FaultReason::Unmapped, Abi::FaultReason::Unaligned}, Abi::FaultReason::Unmapped, 1); RunBdaDwordWriteTests(context); RunBdaAnchorTests(context); } @@ -551,9 +565,9 @@ void RunBdaStoreExecutionTests(const Context& context) { Abi::Fault report{}; std::memcpy(&report, fault.Bytes().data(), sizeof(report)); if (reason == Abi::FaultReason{}) { - Require(report.state == Abi::FaultState::Empty && report.reason == Abi::FaultReason{} && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.reserved == 0, label + "mapped store published a fault"); + Require(report.state == Abi::FaultState::Empty && report.reason == Abi::FaultReason{} && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.otherFaults == 0, label + "mapped store published a fault"); } else { - Require(report.state == Abi::FaultState::Ready && report.reason == reason && report.address == faultAddress && report.bytes == faultBytes && report.stage == static_cast(ShaderRecompiler::IrShaderStage::Compute) && report.instruction == 0x1234 && report.reserved == 0, label + "incorrect first fault metadata"); + Require(report.state == Abi::FaultState::Ready && report.reason == reason && report.address == faultAddress && report.bytes == faultBytes && report.stage == static_cast(ShaderRecompiler::IrShaderStage::Compute) && report.instruction == 0x1234 && report.otherFaults == 1, label + "incorrect first fault metadata"); } std::array words{}; std::memcpy(words.data(), fault.Bytes().data(), sizeof(words)); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 920ca899f3..6d625cf165 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -3238,6 +3238,27 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { reject([&] { resources.WriteBack(); }, "rect-list requires"); Require(guest[0] == 123, "failed GPU command published writes"); } + { + const auto fail = [&](ShaderRecompiler::BdaAbi::FaultReason reason, std::uint32_t word, std::uint32_t value, std::uint32_t otherFaults = 0) { + ShaderResources resources(context, compiled); + std::memcpy(access.bytes(access.descriptor(6).buffer).data() + word * sizeof(std::uint32_t), &value, sizeof(value)); + const ShaderRecompiler::BdaAbi::Fault report{ShaderRecompiler::BdaAbi::FaultState::Ready, reason, 0x7fff99880002ULL, 4, 0, 0x5c8, otherFaults}; + std::memcpy(access.bytes(access.descriptor(5).buffer).data(), &report, sizeof(report)); + try { + resources.WriteBack(); + } catch (const BdaUnalignedAccess& error) { + Require(std::string(error.what()).find("BDA access failed") != std::string::npos, "an unaligned BDA access did not report the failed access"); + return true; + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("BDA access failed") != std::string::npos, "a BDA fault did not report the failed access"); + return false; + } + throw std::runtime_error("a recorded BDA fault was not reported"); + }; + Require(fail(ShaderRecompiler::BdaAbi::FaultReason::Unaligned, 2, 777) && guest[2] == 777, "an unaligned BDA access lost the stores of the dispatch's other invocations"); + Require(!fail(ShaderRecompiler::BdaAbi::FaultReason::Unmapped, 3, 888) && guest[3] == 0, "a failed GPU command published writes"); + Require(!fail(ShaderRecompiler::BdaAbi::FaultReason::Unaligned, 5, 999, 1) && guest[5] == 0, "an unaligned BDA access that hid a fault of another kind published writes"); + } { auto aligned = context; aligned.limits.minStorageBufferOffsetAlignment = 16; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp index 77707e646e..9ca39a5b01 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp @@ -134,6 +134,14 @@ std::vector MakeBdaWriteTestShader(std::uint64_t address, std::ui return shader; } +std::vector MakeBdaFaultSequenceTestShader(std::initializer_list reasons) { + using namespace ShaderRecompiler; + return MakeShader(0u, false, true, [&](SpirvValueEmitContext& ctx, const IrValue&, std::uint32_t base) { + for (const auto reason : reasons) RecordBdaFault(ctx.state, base, ConstantU32(ctx.state, 4u), ConstantU32(ctx.state, 0x1234u), reason); + return std::vector{ConstantU32(ctx.state, 1u)}; + }); +} + std::vector MakeBdaAnchoredReadTestShader(const BdaAnchoredReadProgram& reads, bool anchored) { using namespace ShaderRecompiler; IrProgram program; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp index 555397c843..e31020caf7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp @@ -2,12 +2,15 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_BDASHADER_HPP #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "BdaAbi.hpp" #include +#include #include std::vector MakeBdaTestShader(std::uint64_t address, std::uint32_t bits, std::int64_t offset = 0); std::vector MakeBdaDwordReadTestShader(std::uint64_t address, std::uint32_t dwords, bool coherent, bool stops); std::vector MakeBdaSpanReadTestShader(std::uint64_t address, std::uint32_t offset, std::uint32_t extracted, bool coherent, bool stops); +std::vector MakeBdaFaultSequenceTestShader(std::initializer_list reasons); std::vector MakeBdaDwordWriteTestShader(std::uint64_t address, std::uint32_t dwords, const std::uint32_t* values); std::vector MakeBdaStoreTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent); std::vector MakeBdaWriteTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent); diff --git a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp index b27be01d0e..eb6a6b104b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp @@ -260,8 +260,17 @@ void CheckOptionalInvalid() { Reject([&] { capture.WriteOptional(directory.path); }, "optional capture swallowed a non-directory existing record"); Require(AgcDriver::ComputePipelineCapture::OptionalEnabled() && std::filesystem::file_size(path.parent_path()) == 1u, "non-directory record disabled optional capture or was changed"); Directory unexpected; + const auto finder = unexpected.path / ".DS_Store"; + std::ofstream(finder, std::ios::binary).put('x'); + capture.specialization[0].value = 1u; + Require(!capture.Write(unexpected.path).empty(), "strict capture rejected a Finder metadata file"); + capture.specialization[0].value = 2u; + Require(!capture.WriteOptional(unexpected.path).empty() && AgcDriver::ComputePipelineCapture::OptionalEnabled(), "optional capture rejected a Finder metadata file"); + Require(std::filesystem::file_size(finder) == 1u, "Finder metadata file was changed"); + capture.specialization[0].value = 3u; const auto foreign = unexpected.path / "foreign"; std::ofstream(foreign, std::ios::binary).put('x'); + RejectWithMessage([&] { capture.Write(unexpected.path); }, "capture directory contains an unexpected entry"); Reject([&] { capture.WriteOptional(unexpected.path); }, "optional capture swallowed an unexpected directory entry"); Require(AgcDriver::ComputePipelineCapture::OptionalEnabled() && std::filesystem::file_size(foreign) == 1u, "unexpected entry disabled optional capture or was changed"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp index d660dc354d..0c44fbf067 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp @@ -29,6 +29,102 @@ bool foldsTo(std::uint32_t word, bool expected, bool maskedShift) { return result->HasImmediate() && result->ImmediateBool() == expected; } +bool masksFold(std::uint32_t inner, std::uint32_t outer) { + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& sum = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(1u), &lane}); + auto& first = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&sum, &builder.Constant(inner)}); + auto& second = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&first, &builder.Constant(outer)}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&first})); + auto& user = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&second}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder().Fold(program); + const IrValue* result = user.Argument(0)->Resolve(); + if ((inner & ~outer) == 0u) return result == &first; + return result->Opcode() == IrOpcode::BitwiseAnd32 && result->Argument(0)->Resolve() == &sum && result->Argument(1)->Resolve()->ImmediateU32() == outer; +} + +bool mergedHalfMask() { + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& sum = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(1u), &lane}); + auto& low = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&sum, &builder.Constant(0xffffu)}); + auto& high = builder.Emit(IrOpcode::ShiftLeftLogical32, IrType::U32, {&lane, &builder.Constant(16u)}); + auto& merged = builder.Emit(IrOpcode::BitwiseOr32, IrType::U32, {&low, &high}); + auto& masked = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&merged, &builder.Constant(0x7fffu)}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&low})); + auto& user = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&masked}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder().Fold(program); + const IrValue* result = user.Argument(0)->Resolve(); + return result->Opcode() == IrOpcode::BitwiseAnd32 && result->Argument(0)->Resolve() == ∑ +} + +bool halfExtractions() { + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& low = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(3u), &lane}); + auto& high = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(5u), &lane}); + auto& lowHalf = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&low, &builder.Constant(0xffffu)}); + auto& highHalf = builder.Emit(IrOpcode::ShiftLeftLogical32, IrType::U32, {&high, &builder.Constant(16u)}); + auto& merged = builder.Emit(IrOpcode::BitwiseOr32, IrType::U32, {&lowHalf, &highHalf}); + auto& top = builder.Emit(IrOpcode::ShiftRightLogical32, IrType::U32, {&merged, &builder.Constant(16u)}); + auto& field = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&merged, &builder.Constant(16u), &builder.Constant(16u)}); + auto& bottom = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&merged, &builder.Constant(0u), &builder.Constant(16u)}); + auto& topUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&top}); + auto& fieldUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&field}); + auto& bottomUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&bottom}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder().Fold(program); + const IrValue* topResult = topUser.Argument(0)->Resolve(); + const IrValue* fieldResult = fieldUser.Argument(0)->Resolve(); + const IrValue* bottomResult = bottomUser.Argument(0)->Resolve(); + const bool topMasked = topResult->Opcode() == IrOpcode::BitwiseAnd32 && topResult->Argument(0)->Resolve() == &high && topResult->Argument(1)->Resolve()->ImmediateU32() == 0xffffu; + const bool fieldThrough = fieldResult->Opcode() == IrOpcode::BitFieldUExtract && fieldResult->Argument(0)->Resolve() == &high && fieldResult->Argument(1)->Resolve()->ImmediateU32() == 0u; + const bool bottomThrough = bottomResult->Opcode() == IrOpcode::BitFieldUExtract && bottomResult->Argument(0)->Resolve() == &low; + return topMasked && fieldThrough && bottomThrough; +} + +bool halvesRejoin() { + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& word = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(7u), &lane}); + auto& low = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&word, &builder.Constant(0u), &builder.Constant(16u)}); + auto& high = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&word, &builder.Constant(16u), &builder.Constant(16u)}); + auto& raised = builder.Emit(IrOpcode::ShiftLeftLogical32, IrType::U32, {&high, &builder.Constant(16u)}); + auto& joined = builder.Emit(IrOpcode::BitwiseOr32, IrType::U32, {&low, &raised}); + auto& shifted = builder.Emit(IrOpcode::ShiftLeftLogical32, IrType::U32, {&word, &builder.Constant(16u)}); + auto& back = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&shifted, &builder.Constant(16u), &builder.Constant(16u)}); + auto& joinedUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&joined}); + auto& backUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&back}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder().Fold(program); + const IrValue* rejoined = joinedUser.Argument(0)->Resolve(); + const IrValue* field = backUser.Argument(0)->Resolve(); + const bool fieldBack = field->Opcode() == IrOpcode::BitFieldUExtract && field->Argument(0)->Resolve() == &word && field->Argument(1)->Resolve()->ImmediateU32() == 0u; + return rejoined == &word && fieldBack; +} + bool reversesTo(std::uint32_t word, std::uint32_t expected) { IrProgram program; auto& block = program.CreateBlock(); @@ -118,6 +214,12 @@ int main() { passed &= run("a reversed high bit must fold to bit zero", reversesTo(0x80000000u, 0x00000001u)); passed &= run("a reversed mixed word must fold to its mirror image", reversesTo(0x12345678u, 0x1e6a2c48u)); passed &= run("a reversed descriptor dword must fold to its mirror image", reversesTo(0xdeadbeefu, 0xf77db57bu)); + passed &= run("a repeated mask folds into the first one", masksFold(0xffffu, 0xffffu)); + passed &= run("a narrower mask reads through the wider one", masksFold(0xffffu, 0x7fffu)); + passed &= run("a wider mask after a narrower one is dropped", masksFold(0x00ffu, 0xffffu)); + passed &= run("a mask under a merge of the halves reads through it", mergedHalfMask()); + passed &= run("half extractions read the half that was merged in", halfExtractions()); + passed &= run("the two halves of one word rejoin into the word", halvesRejoin()); return passed ? 0 : 1; } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index a5d0d015da..92a22769c4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -471,6 +471,31 @@ void verifyLongBranch(const std::string& name, std::span co require(source != graph.blocks.end() && source->terminator.kind == TerminatorKind::Branch && graph.FindBlock(source->terminator.trueBlock).startProgramCounter == target, name + ": the jump does not branch to " + std::to_string(target)); } +void verifyScheduledJumpTable() { + const std::vector code{ + 0x83888208u, 0x7e020280u, 0x8f088308u, 0x7e040280u, 0xbe8c1f00u, 0x800cff0cu, 0x00000048u, + 0x820d800du, 0xf4040386u, 0x10000000u, 0xbf8cc07fu, 0xbe901f00u, 0x80100e10u, 0x82110f11u, + 0xbe802010u, 0x7e020281u, 0xbf820003u, 0x7e020282u, 0xbf820001u, 0x7e020283u, 0xe0700000u, + 0x80000100u, 0xbf810000u, 0x0000000cu, 0u, 0x00000014u, 0u, 0x0000001cu, 0u, + }; + verifyJumpTable("scheduled two-dword table", code, 0x20u, {0xcu, 0x14u, 0x1cu}, {0x3cu, 0x44u, 0x4cu}); + static_cast(recompile("scheduled two-dword table", code, 32u)); + static_cast(recompile("scheduled two-dword table", code, 64u)); + auto vcc = patched(code, 0, {0x83ea826au}); + vcc = patched(std::move(vcc), 2, {0x8f6a836au}); + vcc = patched(std::move(vcc), 9, {0xd4000000u}); + verifyJumpTable("scheduled two-dword table through vcc", vcc, 0x20u, {0xcu, 0x14u, 0x1cu}, {0x3cu, 0x44u, 0x4cu}); + static_cast(recompile("scheduled two-dword table through vcc", vcc, 32u)); + static_cast(recompile("scheduled two-dword table through vcc", vcc, 64u)); + const std::vector> refused{ + patched(code, 1, {0xbe880380u}), + patched(code, 3, {0xbe880380u}), + patched(code, 1, {0xbf820001u}), + patched(code, 3, {0xbf850000u}), + }; + for (const auto& rejected : refused) require(refusal(rejected).find("unsupported dynamic s_setpc_b64") != std::string::npos, "a rewritten or conditional two-dword table index was accepted"); +} + void verifyNullSwappc() { const std::array jump{0xbefd210cu, 0xbf810000u}; const auto swappc = RdnaInstructionDecoder{}.Decode(jump).instructions.front(); @@ -1369,6 +1394,7 @@ done: } try { verifyNullSwappc(); + verifyScheduledJumpTable(); VerifyUserDataTail(); VerifyConstantHighTail(); verifyLargeLoopExitFallback(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 67d5b61cbe..6e1f5d23ee 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -12,6 +12,7 @@ #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "ShaderDiskCache.hpp" +#include "ShaderCacheDirectory.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" @@ -51,6 +52,7 @@ #include #include #include +#include #include #include #include @@ -1673,7 +1675,12 @@ void DisabledColorTests() { Require(!state.hasColorTarget && state.color.address == 0 && state.color.bytes == 0, "disabled color writes accessed a color surface"); Require(state.renderExtent.width == 64 && state.renderExtent.height == 4, "attachment-free framebuffer lost screen scissor extent"); queue.context[0xd] = 0; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "empty framebuffer extent"); + const auto emptyScreen = AgcDriver::Graphics::DecodeState(queue); + Require(emptyScreen.renderExtent.width == 1 && emptyScreen.renderExtent.height == 1, "an empty screen scissor without targets did not get a one-pixel framebuffer"); + Require(emptyScreen.scissor.extent.width == 0 && emptyScreen.scissor.extent.height == 0, "an empty screen scissor without targets rasterized pixels"); + queue.context[0xd] = 64u; + const auto emptyRows = AgcDriver::Graphics::DecodeState(queue); + Require(emptyRows.renderExtent.width == 64 && emptyRows.scissor.extent.height == 0, "a screen scissor without rows rasterized pixels"); queue = makeState(); queue.context[0x8e] = 3; const auto partial = AgcDriver::Graphics::DecodeState(queue); @@ -2596,6 +2603,28 @@ void DepthBoundsBiasTests() { queue.context[0x009] = bits(0.75f); auto state = AgcDriver::Graphics::DecodeState(queue); Require(state.depthTest && state.depthBoundsTest && state.minDepthBounds == 0.25f && state.maxDepthBounds == 0.75f, "depth bounds decode changed"); + const auto infinity = std::numeric_limits::infinity(); + for (const auto bounds : {std::array{0.0f, infinity}, std::array{-infinity, 1.0f}, std::array{-infinity, infinity}}) { + queue.context[0x008] = bits(bounds[0]); + queue.context[0x009] = bits(bounds[1]); + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.depthBoundsTest && state.minDepthBounds == bounds[0] && state.maxDepthBounds == bounds[1], "infinite depth bounds changed"); + AgcDriver::Graphics::Context context{}; + expectFailure([&] { AgcDriver::Graphics::ValidateDepthBounds(context, state); }, "depth bounds outside [0, 1]"); + context.depthRangeUnrestricted = true; + AgcDriver::Graphics::ValidateDepthBounds(context, state); + } + for (const auto offset : {0x008u, 0x009u}) { + queue.context[0x008] = bits(0.25f); + queue.context[0x009] = bits(0.75f); + queue.context[offset] = bits(std::numeric_limits::quiet_NaN()); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "NaN depth bound"); + } + queue.context[0x008] = bits(0.25f); + queue.context[0x009] = bits(0.75f); + queue.context[0x00b] = bits(infinity); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "non-finite register"); + queue.context[0x00b] = bits(0.0f); queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); @@ -4814,6 +4843,7 @@ AgcDriver::Graphics::Context mockContext() { context.limits.maxPerStageDescriptorStorageBuffers = 16; context.limits.maxPerStageResources = 128; context.limits.maxDescriptorSetStorageBuffers = 32; + context.limits.maxImageArrayLayers = 2048; return context; } @@ -8721,6 +8751,14 @@ void unorm10_11_11TargetTests() { auto plainState = state; plainState.colors[0].packing = ColorExportPacking::None; expectFailure([&] { validate(packed, plainState); }, "float4 colors"); + auto targetless = plainState; + targetless.colors.clear(); + targetless.blends.clear(); + Require(validate(packed, targetless) == std::set{0u}, "a uint4 export from a draw with no color target was refused"); + auto gap = plainState; + gap.blends.insert(gap.blends.begin(), gap.blends.front()); + gap.colors[0].exportIndex = 1; + Require(validate(packed, gap) == std::set{0u}, "a uint4 export into the unused slot below a color target was refused"); auto uintExport = request; uintExport.context.pixel->targetOutputMode[0] = 7; expectFailure([&] { static_cast(ShaderRecompiler::Recompile(uintExport)); }, "fragment output 0 has no color export packing path"); @@ -9721,21 +9759,36 @@ void pipelineStoreLazyBufferPoolTests() { Require(mock.live == 0, "lazy buffer pool pipeline tests leaked Vulkan objects"); } -void pipelineJournalTests() { +void pipelineJournalTests(bool longPath = false, bool extended = false) { using namespace AgcDriver::Graphics; using Stage = ShaderRecompiler::ShaderStage; - const auto directory = std::filesystem::temp_directory_path() / ("aps5-pipeline-journal-mock-" + std::to_string(std::random_device{}())); + const auto root = std::filesystem::temp_directory_path() / ("aps5-pipeline-journal-mock-" + std::to_string(std::random_device{}())); + auto directory = root; + if (longPath) { + Require(root.native().size() < 180, "the temporary root is too long for the journal path fixture"); + directory /= std::filesystem::path(u8"cache-\u00e4\u6f22"); + directory /= std::string(229 - directory.native().size(), 'd'); + } std::filesystem::create_directories(directory); struct Cleanup { std::filesystem::path directory; + std::filesystem::path journal; ~Cleanup() { + ShaderRecompiler::ShaderDiskCache::Shutdown(); std::error_code error; + if (!journal.empty()) { + std::filesystem::remove(journal, error); + auto lock = journal; + lock += ".lock"; + std::filesystem::remove(lock, error); + } std::filesystem::remove_all(directory, error); } - } cleanup{directory}; + } cleanup{root, {}}; #ifdef _WIN32 + if (extended) directory = std::filesystem::path(L"\\\\?\\" + directory.make_preferred().native()); _putenv_s("ANYPS5_NO_SHADER_CACHE", "0"); - _putenv_s("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str()); + _wputenv_s(L"ANYPS5_SHADER_CACHE_DIR", directory.c_str()); _putenv_s("APS5_PIPELINE_JOURNAL_THREADS", "2"); #else setenv("ANYPS5_NO_SHADER_CACHE", "0", 1); @@ -9743,6 +9796,14 @@ void pipelineJournalTests() { setenv("APS5_PIPELINE_JOURNAL_THREADS", "2", 1); #endif Require(PipelineJournalEnabled(), "the journal is disabled with a shader cache directory set"); + auto journalFile = PipelineJournalPath(); +#ifdef _WIN32 + std::error_code pathError; + journalFile = ShaderRecompiler::NativeCachePath_nid_no_patch(journalFile, pathError); + Require(!pathError, "cannot normalize the journal fixture path"); +#endif + cleanup.journal = journalFile; + if (longPath) Require(journalFile.native().size() > 260, "the journal fixture did not cross MAX_PATH"); const auto module = [](std::uint32_t salt) { ShaderRecompiler::RecompileResult result; result.spirv = std::vector{0x07230203u, 0x00010000u, 0u, 8u, 0u, 0x00020011u, 1u, 0x00020011u, 2u + salt}; @@ -9934,13 +9995,15 @@ void pipelineJournalTests() { FlushPipelineJournal(); const auto repeated = PipelineJournalTotals(); Require(repeated.recordedGraphics == recorded.recordedGraphics && repeated.recordedCompute == recorded.recordedCompute && repeated.recordedModules == recorded.recordedModules, "a repeated pipeline was recorded again"); - Require(std::filesystem::file_size(PipelineJournalPath()) > 0, "the journal file is empty"); + Require(std::filesystem::file_size(journalFile) > 0, "the journal file is empty"); const auto storeBefore = PipelineStoreTotals(); std::vector receivedWords; ShaderRecompiler::RecompileResult received; bool receivedPush = false; std::uint32_t computeCalls = 0; + std::mutex computeMutex; PipelineJournalHooks hooks{[&](const ShaderRecompiler::RecompileResult& shader, bool pushConstants) { + std::lock_guard lock(computeMutex); ++computeCalls; received = shader; receivedPush = pushConstants; @@ -9963,7 +10026,7 @@ void pipelineJournalTests() { const auto storeHit = PipelineStoreTotals(); Require(pipeline != nullptr && !built && mock.pipelineCreateCount == created && storeHit.hits == storeReplayed.hits + 1 && storeHit.misses == storeReplayed.misses, "the draw's configuration did not find the replayed pipeline"); { - const auto journalPath = PipelineJournalPath(); + const auto& journalPath = journalFile; const auto whole = std::filesystem::file_size(journalPath); Require(whole % 8 == 0 && whole > 3, "the journal file is not record aligned"); std::filesystem::resize_file(journalPath, whole - 3); @@ -9977,9 +10040,33 @@ void pipelineJournalTests() { torn->Wait(); const auto reloaded = PipelineJournalTotals(); Require(reloaded.droppedRecords == 0 && reloaded.loadedModules == 4 && reloaded.loadedRecipes == 2 && reloaded.replayFailed == 0, "the journal did not recover from a tail torn inside a record's padding"); + const auto writtenBefore = PipelineJournalTotals().writtenBytes; RecordComputePipeline(compute, true); FlushPipelineJournal(); Require(PipelineJournalTotals().recordedCompute == reloaded.recordedCompute + 1 && PipelineJournalTotals().recordedModules == reloaded.recordedModules, "the pipeline whose record was torn was not recorded again, or its module was"); + const auto recordBytes = PipelineJournalTotals().writtenBytes - writtenBefore; + std::vector bytes; + Require(ShaderRecompiler::ReadWholeFile(journalFile, bytes) && recordBytes > 0 && recordBytes <= bytes.size(), "cannot read the re-recorded journal entry"); + { + std::ofstream file(journalFile, std::ios::binary | std::ios::app); + file.write(reinterpret_cast(bytes.data() + bytes.size() - recordBytes), static_cast(recordBytes)); + const std::array tornTail{}; + file.write(tornTail.data(), tornTail.size()); + file.close(); + Require(file.good(), "cannot append an external complete record and torn tail"); + } + auto another = module(5); + another.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 1, std::vector(4))); + const auto beforeAppend = PipelineJournalTotals(); + RecordComputePipeline(another, false); + FlushPipelineJournal(); + const auto appended = PipelineJournalTotals(); + Require(appended.writeFailures == 0 && appended.recordedCompute == beforeAppend.recordedCompute + 1, "the append after an external torn tail failed"); + Require(std::filesystem::file_size(journalFile) == bytes.size() + recordBytes + appended.writtenBytes - beforeAppend.writtenBytes, "tail recovery discarded an external complete record or retained torn bytes"); + auto recovered = StartPipelineJournalReplay(context, fingerprint, hooks); + Require(recovered != nullptr, "the replay after the external append did not start"); + recovered->Wait(); + Require(PipelineJournalTotals().loadedModules == 5 && PipelineJournalTotals().loadedRecipes == 4 && PipelineJournalTotals().droppedRecords == 0, "the external append and torn-tail recovery did not replay cleanly"); } { const auto before = PipelineJournalTotals(); @@ -9992,7 +10079,7 @@ void pipelineJournalTests() { } { const std::uint32_t meshWithoutOutputSize = 2u; - std::fstream file(PipelineJournalPath(), std::ios::in | std::ios::out | std::ios::binary); + std::fstream file(journalFile, std::ios::in | std::ios::out | std::ios::binary); file.seekp(4); file.write(reinterpret_cast(&meshWithoutOutputSize), sizeof(meshWithoutOutputSize)); file.close(); @@ -10035,8 +10122,8 @@ int main(int argc, char** argv) { commandBatchFenceFailureTests(); return 0; } - if (argc == 2 && std::string_view(argv[1]) == "--pipeline-journal") { - pipelineJournalTests(); + if (argc == 2 && (std::string_view(argv[1]) == "--pipeline-journal" || std::string_view(argv[1]) == "--pipeline-journal-long-paths" || std::string_view(argv[1]) == "--pipeline-journal-extended-paths")) { + pipelineJournalTests(std::string_view(argv[1]) != "--pipeline-journal", std::string_view(argv[1]) == "--pipeline-journal-extended-paths"); std::cout << "pipeline journal recipe, record and replay tests passed\n"; return 0; } @@ -10210,6 +10297,11 @@ int main(int argc, char** argv) { std::cout << "Multisampled DCC state checks passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--depth-bounds-state") { + DepthBoundsBiasTests(); + std::cout << "Depth bounds state checks passed\n"; + return 0; + } { const AgcDriver::Graphics::Context context{}; const AgcDriver::Graphics::State state{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp index 73c243f198..89d83afd29 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp @@ -258,6 +258,16 @@ void RunUnmappedGapTests() { Require(!GuestMemory::Accessible(reinterpret_cast(std::uintptr_t{0x18}), 4), "a near-null address counts as accessible"); munmap(block, page); munmap(block + 2 * page, page); + alignas(4096) static std::array imagePage{}; + const auto imageAddress = reinterpret_cast(imagePage.data()); + Require(GuestMemory::Accessible(imagePage.data(), page, true), "a main image data page is not writable"); + Require(mprotect(imagePage.data(), page, PROT_READ) == 0, "cannot protect the main image test page behind the registry"); + Require(GuestMemory::Accessible(imagePage.data(), page, true), "a checked main image page was queried again instead of read from the page table"); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(imageAddress, page); + Require(!GuestMemory::Accessible(imagePage.data(), page, true) && GuestMemory::Accessible(imagePage.data(), page), "an invalidated main image page kept its old access"); + Require(mprotect(imagePage.data(), page, PROT_READ | PROT_WRITE) == 0, "cannot restore the main image test page"); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(imageAddress, page); + Require(GuestMemory::Accessible(imagePage.data(), page, true), "a restored main image page is not writable"); if (!GuestArena::GuestArenaAvailable_nid_postfix()) return; auto* guest = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(2 * page, page)); Require(mmap(guest, 2 * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == guest, "cannot map the arena page test pages"); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 8410c5d9cd..db38d183f5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -290,9 +290,14 @@ void RunGuestTextureResourceTests() { Fields cubeBadArray = cube; cubeBadArray.baseArray = 6; rejectFields(cubeBadArray, "base array past its last array slice"); - Fields cubeNotMultiple = cube; - cubeNotMultiple.depth = 4; - rejectFields(cubeNotMultiple, "multiple of 6"); + Fields cubeExtraSlice = cube; + cubeExtraSlice.depth = 6; + result = DecodeTextureResource(pack(cubeExtraSlice)); + Require(result.baseArray == 0 && result.depthOrLastArray == 6, "a cube descriptor with a slice past its last whole cube lost a slice"); + Require(DescribeSurface(result).imageLayers == 7, "a cube allocation with a slice past its last whole cube omitted it"); + Fields cubePartial = cube; + cubePartial.depth = 4; + rejectFields(cubePartial, "fewer than 6 array slices"); Fields multisampled = base; multisampled.typeRaw = 14; diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index 7b03f83944..ff2a3e40a7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -987,6 +987,42 @@ int main() { return StencilReferenceValues(b.program) == std::vector{0u, 255u} && !StencilReplaySafe(b.program); }); + passed &= run("an unprovable stencil reference replays every eight-bit value", [] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& zero = b.constant(0u); + auto& reference = b.emit(IrOpcode::UndefU32, IrType::U32, {}); + ExportInfo info{}; + info.kind = ExportTargetKind::MrtZ; + info.en = 2u; + info.vm = true; + b.program.Metadata().exportInfo.push_back(info); + ExportFlags flags{0u}; + std::uint64_t bits = 0; + std::memcpy(&bits, &flags, sizeof(flags)); + auto& value = b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&zero, &reference, &zero, &zero}); + b.emit(IrOpcode::SetAttribute, IrType::Void, {&value, &b.truth()}, bits); + const auto values = StencilReferenceValues(b.program); + return values.size() == 256u && values.front() == 0u && values.back() == 255u; + }); + + passed &= run("a proven stencil reference replays its eight-bit value", [] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& zero = b.constant(0u); + ExportInfo info{}; + info.kind = ExportTargetKind::MrtZ; + info.en = 2u; + info.vm = true; + b.program.Metadata().exportInfo.push_back(info); + ExportFlags flags{0u}; + std::uint64_t bits = 0; + std::memcpy(&bits, &flags, sizeof(flags)); + auto& value = b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&zero, &b.constant(300u), &zero, &zero}); + b.emit(IrOpcode::SetAttribute, IrType::Void, {&value, &b.truth()}, bits); + return StencilReferenceValues(b.program) == std::vector{44u}; + }); + passed &= run("stencil replay admits own-lane masks and their whole-quad expansion as export guards", [&] { Builder b; b.program.Resources().stage = IrShaderStage::Pixel; diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp index 41824053d6..efad88d641 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp @@ -1,9 +1,12 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/TraceFile.hpp" #include "zlib.h" +#include +#include #include #include #include +#include namespace { @@ -346,6 +349,175 @@ void IntervalDroppedThreads() { Check(line.find("\"queue worker 0x0\":{\"events_lost\":5,\"totals_full\":false}") != std::string::npos && line.find("events_full_after_s") == std::string::npos, "an interval without presents printed a loss time"); } +void SummaryMachineFields() { + const auto origin = Clock::now(); + AgcDriver::TraceSummary summary; + summary.commit = "abc"; + summary.dirty = false; + summary.gpu = "GPU"; + summary.driver = "Driver 1"; + summary.os = "OS 1"; + summary.width = 1920; + summary.height = 1080; + summary.window = "summary_every 30"; + summary.host = AgcDriver::TraceHost{"CPU \"X\"", 16, 32000}; + summary.title = AgcDriver::TraceTitle{"PPSA00000", "Example", "01.002.000"}; + const std::vector presents{origin, origin + milliseconds(16)}; + const auto line = TraceFormat::Summary(summary, {}, presents); + Check(line.find("\"os\":\"OS 1\",\"cpu\":\"CPU \\\"X\\\"\",\"cpu_threads\":16,\"memory_mib\":32000,\"title_id\":\"PPSA00000\",\"title\":\"Example\",\"content_version\":\"01.002.000\",\"resolution\":\"1920x1080\"") != std::string::npos, "the summary line did not name the machine and title"); + summary.title.reset(); + Check(TraceFormat::Summary(summary, {}, presents).find("title_id") == std::string::npos, "a summary without param.json named a title"); + summary.host.reset(); + const auto bare = TraceFormat::Summary(summary, {}, presents); + Check(bare.find("\"os\":\"OS 1\",\"resolution\":\"1920x1080\"") != std::string::npos && bare.find("cpu") == std::string::npos, "a summary on a host without a CPU name changed its fields"); +} + +void FrameMicroseconds() { + const auto origin = Clock::now(); + const std::vector presents{origin, origin + microseconds(16667), origin + microseconds(50000), origin + microseconds(50000)}; + Check(TraceFormat::FrameMicroseconds(presents) == std::vector{16667, 33333, 0}, "frame intervals in microseconds changed"); + Check(TraceFormat::FrameMicroseconds({}).empty(), "no presents gave frame intervals"); +} + +void BenchFormat() { + AgcDriver::TraceSummary summary; + summary.commit = "abc"; + summary.environment = {{"APS5_TRACE_BENCH", "1"}}; + summary.gpu = "GPU"; + summary.driver = "Driver 1"; + summary.os = "OS 1"; + summary.host = AgcDriver::TraceHost{"CPU", 8, 16000}; + const AgcDriver::TraceBenchIntervals intervals{ + std::make_shared(AgcDriver::TraceBenchInterval{0, 30.5, 0, 1920, 1080, 120.25, {16667, 16666}}), + std::make_shared(AgcDriver::TraceBenchInterval{30.5, 61, 1830, 2560, 1440, 98.5, {}}), + }; + const std::string expected = + "{\"format\":\"aps5-bench/1\",\"commit\":\"abc\",\"dirty\":null,\"env\":{\"APS5_TRACE_BENCH\":\"1\"},\"gpu\":\"GPU\",\"driver\":\"Driver 1\",\"os\":\"OS 1\"," + "\"cpu\":\"CPU\",\"cpu_threads\":8,\"memory_mib\":16000,\"intervals_dropped\":3,\"intervals\":[\n" + "{\"start_s\":0.000,\"end_s\":30.500,\"first_present\":0,\"resolution\":\"1920x1080\",\"gpu_busy_ms\":120.250,\"frame_us\":[16667,16666]},\n" + "{\"start_s\":30.500,\"end_s\":61.000,\"first_present\":1830,\"resolution\":\"2560x1440\",\"gpu_busy_ms\":98.500,\"frame_us\":[]}\n]}\n"; + Check(TraceFormat::Bench(summary, intervals, 3) == expected, "benchmark file changed"); +} + +void BenchRotation() { + AgcDriver::TraceBenchIntervals kept; + std::uint64_t dropped = 0; + for (std::size_t index = 0; index < AgcDriver::TraceBench::MaximumIntervals; ++index) AgcDriver::TraceBench::Keep(kept, std::make_shared(AgcDriver::TraceBenchInterval{static_cast(index)}), dropped); + Check(kept.size() == AgcDriver::TraceBench::MaximumIntervals && dropped == 0, "intervals were dropped before the file was full"); + for (std::size_t index = 0; index < 5; ++index) AgcDriver::TraceBench::Keep(kept, std::make_shared(AgcDriver::TraceBenchInterval{static_cast(AgcDriver::TraceBench::MaximumIntervals + index)}), dropped); + Check(kept.size() == AgcDriver::TraceBench::MaximumIntervals && dropped == 5, "a full file did not drop one interval per new one"); + Check(kept.front()->startS == 5 && kept.back()->startS == static_cast(AgcDriver::TraceBench::MaximumIntervals + 4), "the file did not keep the newest intervals"); + const auto file = TraceFormat::Bench({}, kept, dropped); + Check(file.find("\"intervals_dropped\":5,") != std::string::npos && file.find("{\"start_s\":5.000,") != std::string::npos && file.find("{\"start_s\":4.000,") == std::string::npos, "the file does not match the kept intervals"); +} + +void BenchWriterKeepsNewest() { + std::mutex mutex; + std::condition_variable changed; + bool entered = false; + bool release = false; + std::vector written; + AgcDriver::TraceBenchWriter writer([&](const AgcDriver::TraceBenchSnapshot& snapshot) { + std::unique_lock lock(mutex); + written.push_back(snapshot.dropped); + entered = true; + changed.notify_all(); + changed.wait(lock, [&] { return release; }); + }); + writer.Write({{}, {}, 1}); + { + std::unique_lock lock(mutex); + changed.wait(lock, [&] { return entered; }); + } + writer.Write({{}, {}, 2}); + writer.Write({{}, {}, 3}); + writer.Write({{}, {}, 4}); + { + std::lock_guard lock(mutex); + release = true; + } + changed.notify_all(); + writer.Flush(); + writer.Stop(); + Check(written == std::vector{1, 4}, "a benchmark snapshot was written after a newer one replaced it"); + Check(writer.Superseded() == 2, "replaced benchmark snapshots were not counted"); +} + +void BenchWriterStopWrites() { + std::vector written; + { + AgcDriver::TraceBenchWriter writer([&](const AgcDriver::TraceBenchSnapshot& snapshot) { written.push_back(snapshot.dropped); }); + writer.Write({{}, {}, 7}); + } + Check(written == std::vector{7}, "stopping the benchmark writer lost its pending snapshot"); +} + +std::string ReadGzip(const std::filesystem::path& path) { + std::ifstream input(path, std::ios::binary); + const std::vector bytes((std::istreambuf_iterator(input)), std::istreambuf_iterator()); + Check(bytes.size() > 18, "the gzip file is truncated"); + std::size_t size = 0; + for (unsigned index = 0; index < 4; ++index) size |= static_cast(bytes[bytes.size() - 4 + index]) << (8 * index); + z_stream stream{}; + Check(inflateInit2(&stream, -MAX_WBITS) == Z_OK, "inflateInit2 failed"); + std::string inflated(size, 0); + stream.next_in = const_cast(bytes.data() + 10); + stream.avail_in = static_cast(bytes.size() - 18); + stream.next_out = reinterpret_cast(inflated.data()); + stream.avail_out = static_cast(inflated.size()); + const int status = inflate(&stream, Z_FINISH); + inflateEnd(&stream); + Check(status == Z_STREAM_END && stream.avail_out == 0, "the gzip file does not inflate to its size"); + return inflated; +} + +void ReplaceGzipFile() { + const auto directory = std::filesystem::temp_directory_path() / ("aps5-bench-test-" + std::to_string(Clock::now().time_since_epoch().count())); + std::filesystem::create_directories(directory); + const auto path = directory / "aps5-bench.json.gz"; + auto temporary = path; + temporary += ".tmp"; + AgcDriver::TraceFile::ReplaceGzip(path, "{\"first\":1}\n"); + AgcDriver::TraceFile::ReplaceGzip(path, "{\"second\":2}\n"); + Check(ReadGzip(path) == "{\"second\":2}\n" && !std::filesystem::exists(temporary), "the benchmark file was not replaced"); + const auto blocked = directory / "aps5-bench-blocked.json.gz"; + auto blockedTemporary = blocked; + blockedTemporary += ".tmp"; + std::filesystem::create_directory(blocked); + bool failed = false; + try { AgcDriver::TraceFile::ReplaceGzip(blocked, "{\"third\":3}\n"); } catch (const std::system_error&) { failed = true; } + const bool blockedKept = std::filesystem::is_directory(blocked); + const bool temporaryLeft = std::filesystem::exists(blockedTemporary); + const auto kept = ReadGzip(path); +#ifndef _WIN32 + // A directory at .tmp makes the write itself fail, which is the case ReplaceGzip exists + // for: the previous file must survive, so an in-place rewrite fails this check. The failure is + // a std::ios_base::failure thrown inside libstdc++'s ofstream, which MinGW builds could not + // unwind to this catch, so the case runs on the other platforms only. + std::filesystem::create_directory(temporary); + bool writeFailed = false; + try { AgcDriver::TraceFile::ReplaceGzip(path, "{\"third\":3}\n"); } catch (const std::exception&) { writeFailed = true; } + const auto keptAfterWrite = ReadGzip(path); +#endif + std::filesystem::remove_all(directory); + Check(failed && blockedKept && !temporaryLeft, "a replace that could not move its file over the destination did not fail cleanly or left its temporary file"); + Check(kept == "{\"second\":2}\n", "a failed replace of another file damaged the benchmark file"); +#ifndef _WIN32 + Check(writeFailed && keptAfterWrite == "{\"second\":2}\n", "a failed rewrite damaged the previous benchmark file"); +#endif +} + +void BenchSwitch() { + Check(!AgcDriver::TraceBench::Parse(nullptr, false), "the benchmark file was enabled without its switch"); + Check(AgcDriver::TraceBench::Parse("1", true), "APS5_TRACE_BENCH=1 was not accepted"); + for (const auto& [text, every] : {std::pair{"1", false}, std::pair{"0", true}, std::pair{"yes", true}, std::pair{"", true}}) { + bool rejected = false; + try { AgcDriver::TraceBench::Parse(text, every); } + catch (const std::invalid_argument&) { rejected = true; } + Check(rejected, "a malformed or unsupported APS5_TRACE_BENCH was accepted"); + } +} + void GzipFile() { std::string text; for (int index = 0; index < 20000; ++index) text += "{\"name\":\"Driver.Packet\",\"ph\":\"X\",\"ts\":" + std::to_string(index) + "}\n"; @@ -413,7 +585,15 @@ int main() { TotalsSince(); IntervalSummaryFields(); IntervalDroppedThreads(); + SummaryMachineFields(); + FrameMicroseconds(); + BenchFormat(); + BenchRotation(); + BenchWriterKeepsNewest(); + BenchWriterStopWrites(); + BenchSwitch(); GzipFile(); + ReplaceGzipFile(); AppendLines(); std::cout << "performance trace tests passed\n"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/ProfileOutput.cpp b/core/libs/prx/libSceAgcDriver/tests/ProfileOutput.cpp index 72fb292b4e..0314674b2a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ProfileOutput.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ProfileOutput.cpp @@ -90,13 +90,17 @@ void ConcurrentWriters() { } void WriterFailure() { - AgcDriver::ProfileOutput output([](std::string_view) { throw std::runtime_error("test writer failure"); }); + AgcDriver::ProfileOutput output([](std::string_view) { throw std::runtime_error("test writer failure"); }, 8192); output.Write("test"); - for (int operation = 0; operation < 2; ++operation) { + for (int operation = 0; operation < 6; ++operation) { bool failed = false; try { if (operation == 0) output.Flush(); - else output.Write("next"); + else if (operation == 1) output.Write("next"); + else if (operation == 2) { output.Stop(); output.Write("after failed stop"); } + else if (operation == 3) output.Print("late failure"); + else if (operation == 4) output.Print("%s", std::string(6000, 'x').c_str()); + else output.Print("%s", std::string(10000, 'x').c_str()); } catch (const std::runtime_error& error) { failed = std::string(error.what()) == "test writer failure"; } @@ -106,16 +110,44 @@ void WriterFailure() { void ExplicitStop() { std::string written; - AgcDriver::ProfileOutput output([&](std::string_view text) { written += text; }); + AgcDriver::ProfileOutput output([&](std::string_view text) { written += text; }, 8192); output.Write("pending output"); output.Stop(); output.Stop(); output.Flush(); Check(written == "pending output", "explicit stop did not drain pending output"); - bool failed = false; - try { output.Write("after stop"); } - catch (const std::runtime_error& error) { failed = std::string(error.what()) == "profile output is stopping"; } - Check(failed, "write after stop was accepted"); + Check(!output.Write("after stop"), "write after stop was accepted"); + Check(!output.Write(""), "empty write after stop was accepted"); + output.Print("late small diagnostic %d", 1); + output.Print("%s", std::string(6000, 'x').c_str()); + output.Print("%s", std::string(10000, 'x').c_str()); + output.Flush(); + Check(written == "pending output", "late output reached the stopped writer"); +} + +void StopWithProducers() { + std::string written; + AgcDriver::ProfileOutput output([&](std::string_view text) { written += text; }); + std::atomic accepted{0}; + std::atomic start{false}; + std::vector producers; + for (int thread = 0; thread < 8; ++thread) { + producers.emplace_back([&] { + start.wait(false); + for (int item = 0; item < 1000; ++item) { + if (output.Write("x")) ++accepted; + } + }); + } + Check(output.Write("x"), "initial write was rejected"); + ++accepted; + start.store(true); + start.notify_all(); + output.Stop(); + for (auto& producer : producers) producer.join(); + output.Flush(); + Check(written == std::string(accepted.load(), 'x'), "stop did not drain exactly the accepted output"); + Check(!output.Write("late"), "concurrent stop left the writer open"); } } @@ -127,6 +159,7 @@ int main() { ConcurrentWriters(); WriterFailure(); ExplicitStop(); + StopWithProducers(); std::cout << "profile output tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index a5322a1921..3c749730b7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -39,11 +39,13 @@ #include "VertexRangeProducer_frag_spv.h" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp" #include "BdaAbi.hpp" +#include "ShaderCacheSourceVersion.hpp" #include #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX #endif +#include #include #else #include @@ -57,6 +59,7 @@ #include #include #include +#include #include #include #include @@ -1140,6 +1143,67 @@ void completionCountTests(const Device& device, Recorder& recorder) { Require(memory[0] == 1 && memory[2] == (storeAlways ? 1u : 7u) && memory[16] == 1, "completion stores ran for the wrong labels (overlapped and not-imported ones store, the untouched GPU-stored one skips)"); } +class StderrCapture { +public: + StderrCapture() { + std::fflush(stderr); + file = std::tmpfile(); + Require(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + _dup2(_fileno(file), _fileno(stderr)); +#else + saved = dup(fileno(stderr)); + dup2(fileno(file), fileno(stderr)); +#endif + } + + std::string Finish() { + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + +std::size_t DeferredFailureLines(Recorder& recorder, std::size_t count, const std::function& failure, std::string* log = nullptr) { + StderrCapture capture; + for (std::size_t index = 0; index < count; ++index) recorder.OnComplete(failure); + recorder.Sync(); + const auto text = capture.Finish(); + if (log != nullptr) *log = text; + std::size_t lines = 0; + for (auto at = text.find("deferred write-back failed"); at != std::string::npos; at = text.find("deferred write-back failed", at + 1)) ++lines; + return lines; +} + +void deferredFailureReportTests(Recorder& recorder) { + std::uint32_t serial = 0; + const auto plain = [&serial] { throw std::runtime_error("image table: poisoned entry " + std::to_string(serial++)); }; + const auto unaligned = [](std::uint32_t instruction) { return [instruction] { throw BdaUnalignedAccess("BDA access failed: unaligned", instruction); }; }; + Require(DeferredFailureLines(recorder, 12, plain) == 12, "a deferred failure other than an unaligned BDA access was not printed every time"); + Require(DeferredFailureLines(recorder, 12, unaligned(0x5c8)) == 8, "the first 8 deferred reports of an unaligned BDA access were not the only ones printed"); + std::string log; + Require(DeferredFailureLines(recorder, 1012, unaligned(0x5c8), &log) == 1 && log.find("pc 0x5c8 1024 times so far") != std::string::npos, "the 1024th deferred report of an unaligned BDA access was not the only one printed"); + Require(DeferredFailureLines(recorder, 12, unaligned(0x7a0)) == 8, "unaligned BDA reports at one instruction limited the printing of the reports at another"); + Require(DeferredFailureLines(recorder, 12, plain) == 12, "unaligned BDA reports limited the printing of other deferred failures"); +} + void directCompletionLabelTests(Recorder& recorder) { using namespace AgcDriver::GuestMemory; constexpr std::size_t size = 65536; @@ -2590,6 +2654,7 @@ int trimmedImportTests(const Device& device) { const auto end = address + test.keptEnd; const auto* kept = HostImportFor(context, begin, static_cast(end - begin)); Require(kept != nullptr && kept->base == begin && kept->base + kept->bytes == end, name + ": the larger side is not imported whole"); + const auto keptMemory = kept->memory; if (test.trimmed) Require(kept->memory == original, "the larger side of " + name + " was imported again instead of kept"); else Require(kept->memory != original, "the side of " + name + " whose 2 MiB run the unmap replaced is served from the import that pinned the run's old pages"); requireImportServesGuest(context, recorder, *kept, begin, end, ++salt, "the larger side of " + name); @@ -2609,7 +2674,7 @@ int trimmedImportTests(const Device& device) { const auto from = reinterpret_cast(cut); const auto* remapped = HostImportFor(context, address, bytes); Require(remapped != nullptr && remapped->base == address && remapped->bytes == bytes, name + ": the range mapped again is not imported whole"); - Require(remapped->memory != original, "after " + name + " the pages mapped again are served from the import that pinned the old ones"); + Require(remapped->memory != keptMemory, "after " + name + " the pages mapped again are served from the import made before they were mapped again"); requireImportServesGuest(context, recorder, *remapped, from, from + test.length, ++salt, "the pages mapped again after " + name); requireImportServesGuest(context, recorder, *remapped, begin, end, ++salt, "the larger side after " + name + " and a new mapping"); } @@ -8296,6 +8361,70 @@ void singleCubeTests(const Device& device, Recorder& recorder) { } } +void cubeExtraSliceTests(const Device& device, Recorder& recorder) { + auto context = device.GetContext(); + TextureDetiler detiler(context); + context.detiler = &detiler; + SampleProgram program(context, recorder, SAMPLE_ARRAY_SPV); + const VkComponentMapping identity{}; + std::array words{0x1000u, (56u << 20u) | (3u << 30u), 15u | (63u << 14u), 0xb0000facu, 6u, 0, 0, 0}; + auto resource = DecodeTextureResource(words); + const auto geometry = DescribeSurface(resource); + Require(geometry.imageLayers == 7u, "a cube with a slice past its last whole cube does not allocate it"); + std::vector memory(static_cast(geometry.guestBytes) + 256); + auto* surface = reinterpret_cast((reinterpret_cast(memory.data()) + 255) & ~std::uintptr_t{255}); + for (std::uint32_t layer = 0; layer < geometry.imageLayers; ++layer) { + std::memset(surface + geometry.GuestLayerOffset(layer), 16 * (layer + 1), static_cast(geometry.layerBytes)); + } + resource.baseAddress = reinterpret_cast(surface); + const std::span snapshot(surface, static_cast(geometry.guestBytes)); + Texture texture(context, detiler, resource, identity, snapshot); + auto storage = StorageTexture::Create(context, detiler, resource, 0); + recorder.Keep(storage); + Texture storageView(context, storage, resource, identity); + const auto wordCube = [&words](std::vector& backing, std::uint32_t format, const std::function& word) { + auto cubeWords = words; + cubeWords[1] = (format << 20u) | (3u << 30u); + auto described = DecodeTextureResource(cubeWords); + const auto layout = DescribeSurface(described); + backing.assign(static_cast(layout.guestBytes) + 256, std::byte{}); + auto* base = reinterpret_cast((reinterpret_cast(backing.data()) + 255) & ~std::uintptr_t{255}); + for (std::uint32_t layer = 0; layer < layout.imageLayers; ++layer) { + const auto value = word(layer); + for (std::uint64_t offset = 0; offset < layout.layerBytes; offset += sizeof(value)) std::memcpy(base + layout.GuestLayerOffset(layer) + offset, &value, sizeof(value)); + } + described.baseAddress = reinterpret_cast(base); + return described; + }; + std::vector depthMemory; + const auto depth = wordCube(depthMemory, 22u, [](std::uint32_t layer) { + const float value = static_cast(layer + 1) / 8.0f; + std::uint32_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + return bits; + }); + auto depthStorage = StorageTexture::Create(context, detiler, depth, 0); + recorder.Keep(depthStorage); + Require(Texture::CanDepthCopyFrom(*depthStorage, depth), "a cube with a slice past its last whole cube cannot take a depth copy"); + Texture depthCopy(context, depthStorage, depth, identity, Texture::DepthCopyTag{}); + depthCopy.RefreshDepthCopy(); + constexpr std::array unsignedFloats{960u, 1024u, 1056u, 1088u, 1104u, 1120u, 1136u}; + std::vector repackMemory; + const auto repacked = wordCube(repackMemory, 43u, [&unsignedFloats](std::uint32_t layer) { return unsignedFloats[layer] << 21u; }); + auto repackStorage = StorageTexture::Create(context, detiler, repacked, 0); + recorder.Keep(repackStorage); + Require(Texture::CanRepackCopyFrom(*repackStorage, repacked), "a cube with a slice past its last whole cube cannot take a repacked copy"); + Texture repackCopy(context, repackStorage, repacked, identity, Texture::RepackCopyTag{}); + repackCopy.RefreshDepthCopy(); + for (std::uint32_t layer = 0; layer < geometry.imageLayers; ++layer) { + const auto expected = 16.0f * (layer + 1) / 255.0f; + expectRed(program.Red(texture.View(), texture.Layout(), 0, static_cast(layer)), expected, "a cube snapshot with a slice past its last whole cube sampled the wrong slice"); + expectRed(program.Red(storageView.View(), storageView.Layout(), 0, static_cast(layer)), expected, "a cube storage view with a slice past its last whole cube sampled the wrong slice"); + expectRed(program.Red(depthCopy.View(), depthCopy.Layout(), 0, static_cast(layer)), static_cast(layer + 1) / 8.0f, "a cube depth copy with a slice past its last whole cube sampled the wrong slice"); + expectRed(program.Red(repackCopy.View(), repackCopy.Layout(), 0, static_cast(layer)), static_cast(layer + 1), "a cube repacked copy with a slice past its last whole cube sampled the wrong slice"); + } +} + struct SampledViewProbe { Context& context; PFN_vkGetDeviceProcAddr resolver; @@ -8871,9 +9000,14 @@ int pipelineCacheFileTests(const Device& device) { return after < before + footprints[index] / 2; }; const auto cacheFile = [&] { + for (const auto& entry : std::filesystem::directory_iterator(directory)) Require(entry.is_directory(), "a pipeline cache file lives in the shader cache root"); + char version[17]; + std::snprintf(version, sizeof(version), "%016llx", static_cast(ShaderRecompiler::ShaderDiskCache::Generated::SourceVersion)); + const auto versionDirectory = directory / version; + Require(std::filesystem::is_directory(versionDirectory), "the current shader cache version has no directory"); std::vector found; - for (const auto& entry : std::filesystem::directory_iterator(directory)) found.push_back(entry.path()); - Require(found.size() == 1 && found.front().filename().string().starts_with("vk-pipelines-"), "the pipeline cache directory does not hold exactly one cache file"); + for (const auto& entry : std::filesystem::directory_iterator(versionDirectory)) found.push_back(entry.path()); + Require(found.size() == 1 && found.front().filename().string().starts_with("vk-pipelines-"), "the current shader cache version's directory does not hold exactly one pipeline cache file"); return found.front(); }; const auto contents = [](const std::filesystem::path& path) { @@ -9641,6 +9775,7 @@ int main(int argc, char** argv) { } if (argc == 2 && std::string_view(argv[1]) == "--cube-only") { singleCubeTests(device, recorder); + cubeExtraSliceTests(device, recorder); std::cout << "Single cube snapshot and storage sampling tests passed\n"; return 0; } @@ -9689,6 +9824,11 @@ int main(int argc, char** argv) { std::cout << "Read list tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--deferred-failure-report-only") { + deferredFailureReportTests(recorder); + std::cout << "Deferred failure report tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--host-import-unmap-only") { gpu.unlock(); hostImportUnmapTests(device, recorder); @@ -9911,6 +10051,7 @@ int main(int argc, char** argv) { firstLayerViewTests(device, recorder); sampledStorageBaseLevelUsageTests(device, recorder); singleCubeTests(device, recorder); + cubeExtraSliceTests(device, recorder); atomicViewTests(device, recorder); metadataPassTests(device, recorder); cmaskPassTests(device, recorder); diff --git a/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp index 55fb0a5297..a17b7cf87d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp @@ -256,6 +256,34 @@ int main() { return b.eliminate() == 0u && AllUsed({&plain, &flagged, &otherType, &floatImmediate}); }); + passed &= run("four-operand expressions retain distinct trailing immediates across a large block", [] { + Builder b; + auto& source = b.unknown(); + std::vector reads; + std::vector duplicates; + std::vector different; + for (std::uint32_t index = 0; index < 256u; ++index) { + const auto composite = [&](std::uint32_t last) -> IrValue& { + return b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&source, &b.constant(index), &b.constant(7u), &b.constant(last)}); + }; + auto& first = composite(0u); + auto& second = composite(0u); + auto& changed = composite(1u); + auto& firstRead = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&first, &b.constant(3u)}); + auto& secondRead = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&second, &b.constant(3u)}); + auto& changedRead = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&changed, &b.constant(3u)}); + for (auto* read : {&firstRead, &secondRead, &changedRead}) b.keep(*read); + reads.push_back(&firstRead); + duplicates.push_back(&second); + duplicates.push_back(&secondRead); + different.push_back(&changedRead); + } + if (b.eliminate() != 512u || !AllUsed(different)) return false; + for (const auto* duplicate : duplicates) if (duplicate->HasUses()) return false; + for (const auto* read : reads) if (read->UseCount() != 2u) return false; + return true; + }); + passed &= run("float immediates compare by bits: +0 and -0 and two NaN payloads stay apart, one NaN merges", [] { Builder b; auto& x = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()}); diff --git a/core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp b/core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp new file mode 100644 index 0000000000..e6202ceb1a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp @@ -0,0 +1,88 @@ +#include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp" +#include "Decoder/Png.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void setScreenshotDirectory(const std::filesystem::path& directory) { +#ifdef _WIN32 + require(_putenv_s("ANYPS5_SCREENSHOTS", directory.string().c_str()) == 0, "set ANYPS5_SCREENSHOTS"); +#else + require(::setenv("ANYPS5_SCREENSHOTS", directory.string().c_str(), 1) == 0, "set ANYPS5_SCREENSHOTS"); +#endif +} + +std::vector readFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + require(file.good(), "open the written screenshot"); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +void requestsMerge() { + require(!AgcDriver::Screenshot::Take(), "no screenshot is requested at start"); + AgcDriver::Screenshot::Request(); + AgcDriver::Screenshot::Request(); + require(AgcDriver::Screenshot::Take(), "a requested screenshot is taken"); + require(!AgcDriver::Screenshot::Take(), "repeated requests are taken once"); +} + +void writesRgbPng(const std::filesystem::path& directory) { + const std::array bgra{std::byte{0x10}, std::byte{0x20}, std::byte{0x30}, std::byte{0x00}, std::byte{0xA0}, std::byte{0xB0}, std::byte{0xC0}, std::byte{0x7F}}; + const auto first = AgcDriver::Screenshot::Write("TEST00001", 2, 1, bgra); + const auto second = AgcDriver::Screenshot::Write("TEST00001", 2, 1, bgra); + require(first.parent_path() == directory, "the screenshot is written to ANYPS5_SCREENSHOTS"); + require(first.extension() == ".png" && first.filename().string().starts_with("TEST00001-"), "the screenshot is named after the title ID"); + require(first != second && std::filesystem::exists(second), "a second screenshot does not replace the first"); + const auto image = Decoder::Png::Decode(readFile(first)); + require(image.has_value() && image->width == 2 && image->height == 1, "the screenshot decodes at the frame extent"); + const std::vector expected{0x30, 0x20, 0x10, 0xFF, 0xC0, 0xB0, 0xA0, 0xFF}; + require(image->pixels == expected, "the screenshot holds the frame's colors as opaque RGB"); +} + +void rejectsInvalidFrames() { + const std::array pixel{}; + const auto rejects = [](auto write) { + try { + write(); + } catch (const std::invalid_argument&) { + return true; + } + return false; + }; + require(rejects([&] { AgcDriver::Screenshot::Write("TEST00001", 2, 1, pixel); }), "a frame smaller than its extent is rejected"); + require(rejects([&] { AgcDriver::Screenshot::Write("TEST00001", 0, 1, {}); }), "an empty frame is rejected"); + require(rejects([&] { AgcDriver::Screenshot::Write("", 1, 1, pixel); }), "an empty title ID is rejected"); +} + +} + +int main() try { + const auto root = std::filesystem::temp_directory_path() / ("anyps5_screenshot-" + std::to_string(std::random_device{}())); + const auto directory = root / "nested"; + std::filesystem::remove_all(root); + setScreenshotDirectory(directory); + requestsMerge(); + writesRgbPng(directory); + rejectsInvalidFrames(); + std::filesystem::remove_all(root); + std::cout << "agc screenshot 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/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 1796f83acb..c86a452b79 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -973,6 +973,62 @@ void verifyPersist() { requireSameArtifact(variant.artifact, persisted->artifact, "a persisted entry whose file was replaced and removed"); } +#ifdef _WIN32 +void verifyLongHostPaths() { + using namespace ShaderRecompiler::ShaderDiskCache; + auto directory = std::filesystem::temp_directory_path() / ("aps5-cache-long-" + std::to_string(std::random_device{}())); + require(directory.native().size() < 224, "temporary path is too long for the cache control"); + directory /= std::wstring(224 - directory.native().size(), L'd'); + const auto extended = [](const auto& path) { return std::filesystem::path(L"\\\\?\\" + path.native()); }; + std::filesystem::create_directories(extended(directory)); + setEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); + setEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string()); + std::vector files; + std::filesystem::path entries; + struct Cleanup { + const std::filesystem::path& directory; + const std::vector& files; + const std::filesystem::path& entries; + ~Cleanup() { + ShaderRecompiler::ShaderDiskCache::Shutdown(); + std::error_code error; + for (const auto& file : files) std::filesystem::remove(std::filesystem::path(L"\\\\?\\" + file.native()), error); + if (!entries.empty()) std::filesystem::remove(std::filesystem::path(L"\\\\?\\" + entries.native()), error); + std::filesystem::remove(std::filesystem::path(L"\\\\?\\" + directory.native()), error); + } + } cleanup{directory, files, entries}; + SampleRequest sample; + const auto key = sample.Key(); + const auto variant = sampleVariant(); + entries = ShaderDiskCache::EntryDirectory(); + files.push_back(entries / EntryName(key)); + files.push_back(entries / "last-used"); + require(files.front().native().size() >= MAX_PATH, "the cache persistence control did not cross MAX_PATH"); + const auto persisted = ShaderDiskCache::Persist(key, variant); + std::vector published; + require(ReadWholeFile(files.front(), published) && published == EncodeEntry(key, variant), + "the long-path cache entry was not correctly published before mapping"); + require(persisted != nullptr, "a long-path cache entry was not persisted and mapped"); + MEMORY_BASIC_INFORMATION region{}; + require(VirtualQuery(persisted->artifact.spirv.data(), ®ion, sizeof(region)) == sizeof(region) && region.Type == MEM_MAPPED, + "long-path persisted SPIR-V is not mapped"); + requireSameVariant(variant, *persisted, "long-path persist round trip"); + CompiledVariant loaded; + require(ShaderDiskCache::Load(key, loaded), "a long-path persisted cache entry did not load"); + requireSameVariant(variant, loaded, "long-path load round trip"); + const auto packPath = directory / (std::wstring(75, L'p') + L".pack"); + files.push_back(packPath); + require(WriteFileAtomically(packPath, packBytes({{key, EncodeEntry(key, variant)}})), "cannot publish the long-path cache pack"); + CachePack pack(packPath); + require(pack.Valid(), "a long-path cache pack was rejected: " + pack.Error()); + PackLoadTimings timings; + CompiledVariant decoded; + require(pack.Load(key, decoded, timings) == LoadStatus::Loaded, "a long-path cache pack entry did not load"); + requireSameVariant(variant, decoded, "long-path cache pack round trip"); + std::cout << "long-path shader cache persistence and pack tests passed\n"; +} +#endif + struct ComputeRequest { std::vector code{0xe0700000u, 0x80000000u, 0xbf810000u}; std::array userData{0x10000000u, 0x00000000u, 0x40u, 0x00027facu}; @@ -2321,6 +2377,12 @@ int main(int argc, char** argv) { std::cout << "artifact codec tests passed\n"; return 0; } +#ifdef _WIN32 + if (argc == 2 && std::string_view(argv[1]) == "--long-host-paths") { + verifyLongHostPaths(); + return 0; + } +#endif if (argc == 2 && std::string_view(argv[1]) == "--large-file") return runLargeFileWrite(); if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess(); if (argc == 2 && std::string_view(argv[1]) == "--load-prebuilt") return runPrebuiltLoadingProcess(); diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvFailureText.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvFailureText.cpp new file mode 100644 index 0000000000..10559775ea --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvFailureText.cpp @@ -0,0 +1,75 @@ +#include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/ShaderStageInputInfo.hpp" +#include "SpirvBackend/SpirvEmitter.hpp" +#include "PipelineSpecialization.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +enum class Failure { MissingComputeInput, UnloweredInstruction }; + +std::string emissionFailure(Failure failure) { + IrProgram program; + program.SetWaveSize(32); + program.Resources().stage = IrShaderStage::Compute; + program.Resources().shaderHash = 0x1173ae1000ull; + program.Resources().srtPlanComplete = true; + program.Resources().resourceTrackingComplete = true; + program.Metadata().shaderInfoComplete = true; + program.Metadata().bindingLayoutComplete = true; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + program.Metadata().blockInfo.emplace_back(); + if (failure == Failure::UnloweredInstruction) block.AppendInstruction(&program.CreateValue(IrOpcode::Unreachable, IrType::Void)); + ShaderComputeInputInfo compute; + compute.threadsNum[0] = 32; + compute.threadsNum[1] = 1; + compute.threadsNum[2] = 1; + compute.hostSubgroupSize = 32; + compute.waveSize = 32; + ShaderStageInputInfo inputs; + if (failure != Failure::MissingComputeInput) inputs.compute = &compute; + CompiledBindingLayout bindings; + bindings.layout = program.Metadata().bindings; + SpirvTargetOptions target{}; + target.vulkanVersion = 0x00401000; + target.spirvVersion = 0x00010300; + target.subgroupSize = 32; + try { + static_cast(SpirvEmitter().Emit(program, inputs, bindings, target)); + } catch (const std::runtime_error& error) { + return error.what(); + } + throw std::runtime_error("the program emitted without a failure"); +} + +void requireFailure(Failure failure, std::string_view prefix, std::string_view reason) { + const auto text = emissionFailure(failure); + require(text.starts_with(prefix) && text.find(reason) != std::string::npos, "expected \"" + std::string(prefix) + "...\" with \"" + std::string(reason) + "\", got \"" + text + "\""); +} + +} + +int main() { + try { + requireFailure(Failure::MissingComputeInput, "SPIR-V emission failed: hash=0x1173ae1000 stage=", " reason=compute input info is missing"); + requireFailure(Failure::UnloweredInstruction, "SPIR-V emission failed: hash=0x1173ae1000 stage=", " opcode=Unreachable reason="); + } catch (const std::exception& error) { + std::fprintf(stderr, "SpirvFailureText: %s\n", error.what()); + return 1; + } + std::puts("SpirvFailureText: emission failures name the shader hash in hex"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvSubvectorLoopBegin.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvSubvectorLoopBegin.cpp new file mode 100644 index 0000000000..b0d590b7d7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvSubvectorLoopBegin.cpp @@ -0,0 +1,172 @@ +#define SPV_ENABLE_UTILITY_CODE +#include +#undef SPV_ENABLE_UTILITY_CODE +#include "Recompiler.hpp" +#include "SubvectorLoopFixture.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +std::vector recompile(bool robustBufferAccess2) { + const std::array userData{0x10000000u, 0u, 0x100u, 0x31016facu, 0x10001000u, 0u, 0x900u, 0x31016facu}; + const std::array extensions{"SPV_KHR_storage_buffer_storage_class"}; + const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, SubvectorLoopFixture::ExecKeptCode, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 64; + request.target.bdaAbiVersion = 1; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.target.robustBufferAccess2 = robustBufferAccess2; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + auto result = Recompile(request); + require(!result.spirv.empty(), "the shader did not recompile"); + return result.spirv.Copy(); +} + +enum class EmptyExecTest { + Other, + OneCompare, + TwoCompares +}; + +class BranchConditions { +public: + explicit BranchConditions(std::vector module) : words(std::move(module)) { + require(words.size() >= 5, "the SPIR-V header is missing"); + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + require(count != 0 && count <= words.size() - offset, "a SPIR-V instruction is truncated"); + const auto opcode = static_cast(words[offset] & spv::OpCodeMask); + bool result = false; + bool type = false; + spv::HasResultAndType(opcode, &result, &type); + if (result) definitions.emplace(words[offset + (type ? 2 : 1)], offset); + if (opcode == spv::OpBranchConditional) conditions.push_back(words[offset + 1]); + offset += count; + } + } + + std::vector Tests() const { + std::vector tests; + for (const auto condition : conditions) tests.push_back(classify(condition)); + return tests; + } + +private: + std::vector words; + std::vector conditions; + std::map definitions; + + std::span instruction(std::uint32_t id, spv::Op opcode, std::size_t count) const { + const auto found = definitions.find(id); + if (found == definitions.end()) return {}; + const auto offset = found->second; + if ((words[offset] & spv::OpCodeMask) != static_cast(opcode) || (words[offset] >> spv::WordCountShift) != count) return {}; + return std::span(words).subspan(offset, count); + } + + bool zero(std::uint32_t id) const { + const auto constant = instruction(id, spv::OpConstant, 4); + return !constant.empty() && constant[3] == 0u; + } + + std::optional comparedWithZero(std::uint32_t id) const { + const auto compare = instruction(id, spv::OpIEqual, 5); + if (compare.empty()) return std::nullopt; + if (zero(compare[4])) return compare[3]; + if (zero(compare[3])) return compare[4]; + return std::nullopt; + } + + std::optional> execWord(std::uint32_t id) const { + const auto extract = instruction(id, spv::OpCompositeExtract, 5); + if (extract.empty() || instruction(extract[3], spv::OpGroupNonUniformBallot, 5).empty()) return std::nullopt; + return std::pair{extract[3], extract[4]}; + } + + bool execWords(std::uint32_t first, std::uint32_t second) const { + const auto low = execWord(first); + const auto high = execWord(second); + return low && high && low->first == high->first && low->second <= 1u && high->second <= 1u && low->second != high->second; + } + + EmptyExecTest classify(std::uint32_t condition) const { + if (const auto negation = instruction(condition, spv::OpLogicalNot, 4); !negation.empty()) return classify(negation[3]); + if (const auto operand = comparedWithZero(condition)) { + const auto combined = instruction(*operand, spv::OpBitwiseOr, 5); + if (!combined.empty() && execWords(combined[3], combined[4])) return EmptyExecTest::OneCompare; + } + const auto both = instruction(condition, spv::OpLogicalAnd, 5); + if (!both.empty()) { + const auto first = comparedWithZero(both[3]); + const auto second = comparedWithZero(both[4]); + if (first && second && execWords(*first, *second)) return EmptyExecTest::TwoCompares; + } + return EmptyExecTest::Other; + } +}; + +int failures = 0; + +void fail(const std::string& message) { + std::fprintf(stderr, "SpirvSubvectorLoopBegin: %s\n", message.c_str()); + ++failures; +} + +void check(bool robustBufferAccess2) { + const std::string mode = robustBufferAccess2 ? "robustBufferAccess2 on" : "robustBufferAccess2 off"; + std::uint32_t oneCompare = 0; + std::uint32_t twoCompares = 0; + for (const auto test : BranchConditions(recompile(robustBufferAccess2)).Tests()) { + if (test == EmptyExecTest::OneCompare) ++oneCompare; + if (test == EmptyExecTest::TwoCompares) ++twoCompares; + } + if (twoCompares != 0u) { + fail(mode + ": branches that compare exec_lo and exec_hi with 0 separately: " + std::to_string(twoCompares) + ", expected 0"); + } + if (oneCompare != 1u) { + fail(mode + ": branches that compare (exec_lo | exec_hi) with 0: " + std::to_string(oneCompare) + ", expected 1 (s_subvector_loop_begin)"); + } +} + +} + +int main() { + try { + check(false); + check(true); + } catch (const std::exception& error) { + fail(error.what()); + } + if (failures != 0) return 1; + std::puts("SpirvSubvectorLoopBegin: s_subvector_loop_begin tests an empty exec mask as (exec_lo | exec_hi) == 0 in both robustBufferAccess2 modes"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/SubvectorLoopFixture.hpp b/core/libs/prx/libSceAgcDriver/tests/SubvectorLoopFixture.hpp new file mode 100644 index 0000000000..2438f79b12 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SubvectorLoopFixture.hpp @@ -0,0 +1,21 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SUBVECTORLOOPFIXTURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SUBVECTORLOOPFIXTURE_HPP + +#include +#include + +namespace SubvectorLoopFixture { + +alignas(256) constexpr std::array ExecKeptCode{ + 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, + 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000a, 0x80148114, 0x4a141481, + 0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850001, 0xbe26fff6, + 0xbe98047e, 0xbefe04c1, 0x7e1e0226, 0x7e200218, 0x7e220219, 0x7e240214, 0xe0701000, 0x80010a01, + 0xe0701100, 0x80010b01, 0xe0701200, 0x80010c01, 0xe0701300, 0x80010d01, 0xe0701400, 0x80010e01, + 0xe0701500, 0x80010f01, 0xe0701600, 0x80011001, 0xe0701700, 0x80011101, 0xe0701800, 0x80011201, + 0xbf810000, +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp index 7cbca37e6e..cb439c6ffe 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp @@ -141,27 +141,6 @@ void requireSliceAddresses(const GuestTextureResource& resource, std::uint32_t b } void RunTextureTilingTests() { - { - GuestTextureResource surface{}; - surface.baseAddress = 0x30000000; - surface.width = 1920; - surface.height = 1080; - surface.mipCount = 6; - surface.tileMode = TextureTileMode::kR64KBX; - surface.dimension = TextureDimension::k2D; - surface.format = 71; - const auto tracks = [](const GuestTextureResource& resource) { return TracksBlockUnits(resource, DescribeSurface(resource)); }; - Require(tracks(surface), "a tiled surface without DCC metadata must be tracked as 64 KiB blocks"); - surface.dccAddress = 0x31000000; - Require(!tracks(surface), "a DCC surface with more than one mip must be tracked as one unit"); - surface.mipCount = 1; - Require(tracks(surface), "a DCC surface with one mip must be tracked as 64 KiB blocks"); - surface.baseAddress += 0x1000; - Require(!tracks(surface), "a surface off a 64 KiB boundary must be tracked as one unit"); - surface.baseAddress -= 0x1000; - surface.tileMode = TextureTileMode::kLinear; - Require(!tracks(surface), "a linear surface must be tracked as one unit"); - } { const auto mips = ComputeMipLayout(TextureTileMode::kLinear, 1, 4, 4, 2); Require(mips.size() == 2, "linear mip chain must contain the requested mip count"); @@ -461,4 +440,47 @@ void RunTextureTilingTests() { volume.baseArray = 0; Require(ViewedArrayWindow(volume).baseAddress == cube.baseAddress, "a volume keeps its descriptor"); } + + { + GuestTextureResource array{}; + array.baseAddress = 0x10000000ull; + array.width = 16; + array.height = 16; + array.depthOrLastArray = 3200; + array.baseArray = 1536; + array.mipCount = 2; + array.tileMode = TextureTileMode::kStandard64KB; + array.dimension = TextureDimension::k2DArray; + array.format = 56; + const auto geometry = DescribeSurface(array); + const auto window = ArrayLayerWindow(array, 2048); + Require(window.baseAddress == array.baseAddress + 1536ull * geometry.layerBytes, "a storage view past maxImageArrayLayers must start at its first viewed slice"); + Require(window.baseArray == 0 && window.depthOrLastArray == 1664, "a storage view past maxImageArrayLayers must hold exactly its viewed slices"); + Require(DescribeSurface(window).layerBytes == geometry.layerBytes && DescribeSurface(window).mips[1].tiledOffset == geometry.mips[1].tiledOffset, "a viewed slice must keep the layout of the whole array's slice"); + + auto fitting = array; + fitting.depthOrLastArray = 2047; + const auto kept = ArrayLayerWindow(fitting, 2048); + Require(kept.baseAddress == fitting.baseAddress && kept.baseArray == fitting.baseArray && kept.depthOrLastArray == fitting.depthOrLastArray, "an array within maxImageArrayLayers must keep its descriptor"); + + auto linear = array; + linear.tileMode = TextureTileMode::kLinear; + Require(ArrayLayerWindow(linear, 2048).baseAddress == linear.baseAddress + 1536ull * DescribeSurface(linear).layerBytes, "a linear storage view past maxImageArrayLayers must start at its first viewed slice"); + + auto wide = array; + wide.baseArray = 1000; + reject([&] { static_cast(ArrayLayerWindow(wide, 2048)); }, "more array slices than maxImageArrayLayers"); + auto xorSwizzle = array; + xorSwizzle.tileMode = TextureTileMode::kR64KBX; + reject([&] { static_cast(ArrayLayerWindow(xorSwizzle, 2048)); }, "depends on the slice"); + auto volume = array; + volume.dimension = TextureDimension::k3D; + Require(ArrayLayerWindow(volume, 2048).baseAddress == volume.baseAddress && ArrayLayerWindow(volume, 2048).depthOrLastArray == volume.depthOrLastArray, "a volume deeper than maxImageArrayLayers is not an array and keeps its descriptor"); + auto flat = array; + flat.dimension = TextureDimension::k2D; + Require(ArrayLayerWindow(flat, 2048).baseAddress == flat.baseAddress && ArrayLayerWindow(flat, 2048).depthOrLastArray == flat.depthOrLastArray, "a 2D image is one layer whatever its last array field holds and keeps its descriptor"); + auto cube = array; + cube.dimension = TextureDimension::kCube; + reject([&] { static_cast(ArrayLayerWindow(cube, 2048)); }, "not a 1D or 2D array"); + } } diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 2d4f10f07c..14d6423661 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -1044,6 +1044,37 @@ void CheckImportWindowZeroPages() { Require(changed[4] == BlockWritten && WrittenSince(base + 4 * Block, Block, synced), "a store of new bytes after the zero pages of an import window was missed"); } +void CheckImportWindowOverUnwatchedBlock() { + void* memory = AllocateWatched(4 * Block); + const auto base = reinterpret_cast(memory); + auto* bytes = static_cast(memory); + std::memset(memory, 0x11, 4 * Block); + Unwatch(base + Block, Block); + const auto synced = CollectWritesUncached(base + 2 * Block, 2 * Block); + const auto syncedFirst = CollectWritesUncached(base, Block); + Require(synced != 0 && syncedFirst != 0, "the watched blocks around the unwatched one are not collected"); + bytes[0x500] = 0x44; + Require(ImportWatched(base, 4 * Block, [&] { + bytes[0x100] = 0x11; + bytes[2 * Block + 0x100] = 0x11; + bytes[3 * Block + 0x2000] = 0x22; + return true; + }), "the import over an unwatched block failed"); + Require(!Watched(base + Block, Block), "an import window made an unwatched block watched"); + Require(Watched(base, Block) && Watched(base + 2 * Block, 2 * Block), "an import window over an unwatched block took the watched blocks out of the write watch"); + const std::array generations{synced, synced}; + std::array changed{}; + std::array cpu{}; + Require(ChangedBlocks(base + 2 * Block, 2 * Block, generations, changed, cpu), "the watched blocks after an unwatched block are not tracked"); + Require(changed[0] == BlockUnchanged && UnchangedSince(base + 2 * Block, Block, synced), "a page an import window over an unwatched block resolved without changing its bytes reads as written"); + Require(changed[1] == BlockWritten && WrittenSince(base + 3 * Block, Block, synced), "a store of new bytes within an import window over an unwatched block was missed"); + Require(WrittenSince(base, Block, syncedFirst), "a store before an import window over an unwatched block was missed"); + const auto after = CollectWritesUncached(base, Block); + bytes[0x100] = 0x33; + static_cast(CollectWritesUncached(base, Block)); + Require(after != 0 && !UnchangedSince(base, Block, after), "a store after an import window over an unwatched block was missed"); +} + void ProtectPage(std::uint64_t address, bool readable) { #ifdef _WIN32 DWORD previous = 0; @@ -1327,6 +1358,7 @@ int main() { CheckImportWindow(); CheckImportWindowZeroPages(); CheckImportWindowOtherThread(); + CheckImportWindowOverUnwatchedBlock(); CheckStoreAcrossImports(); CheckUnwatchedImport(); CheckCommittedRanges(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index d871a86171..5c662f4b2d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -629,6 +629,52 @@ void DccFastClearTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSa } } } + for (const auto samples : {2u, 4u, 8u}) { + if (selectedSamples != 0 && samples != selectedSamples) continue; + 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, 0, BlockBytes); + auto first = DecodeTarget(source, samples); + first.dccAddress = metadata.Address(); + first.dccPipeAligned = true; + const Color written{0.25f, 0.25f, 0.25f, 0.25f}; + Draw(device, first, false, Float32Export, Program(written, Float32Export, 1u), false, true); + const std::vector ones(AgcDriver::Graphics::MultisampledDccBytes(first), std::byte{0xc0}); + AgcDriver::GuestMemory::Write(metadata.Address(), ones); + auto second = first; + second.pipeBankXor = 0x4000u; + Draw(device, second, false, Float32Export, Program(written, Float32Export, 1u), false, true); + blocks.push_back(std::make_unique(layout.Bytes())); + auto& resolved = *blocks.back(); + std::memset(resolved.data, 0, resolved.bytes); + AgcDriver::Graphics::ColorMetadataPass pass{}; + pass.mode = AgcDriver::Graphics::ColorMetadataPass::Mode::Resolve; + pass.source = second; + pass.targets = {DecodeTarget(resolved)}; + device.ColorMetadataPass(pass); + const auto stored = ReadBack(device, resolved); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto expected = x < ScissorWidth ? 1.0f + (written[channel] - 1.0f) / static_cast(samples) : 1.0f; + Require(FromHalf(stored[layout.Offset(x, y) / 2u + channel]) == expected, "a multisampled DCC target recreated with another pipe-bank XOR did not start from its new fast clear"); + } + } + } + auto third = second; + third.dccAlphaOnMsb = true; + bool refused = false; + try { + Draw(device, third, false, Float32Export, Program(written, Float32Export, 1u), false, true); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find("layout or metadata changed") != std::string_view::npos; + } + Require(refused, "a resident multisampled DCC target accepted a use with other DCC metadata"); + programCount = 0; + } for (const auto samples : {2u, 4u, 8u}) { if (selectedSamples != 0 && samples != selectedSamples) continue; for (const auto mode : {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u}) { @@ -818,6 +864,56 @@ void DccRefillTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSampl } } +void DccImportWindowTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSamples) { + if (!AgcDriver::GuestMemory::WriteWatched()) return; + 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 Color written{0.25f, 0.25f, 0.25f, 0.25f}; + for (const auto samples : {2u, 4u, 8u}) { + if (selectedSamples != 0 && samples != selectedSamples) continue; + 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, 0, BlockBytes); + auto target = DecodeTarget(source, samples); + target.dccAddress = metadata.Address(); + target.dccPipeAligned = true; + target.dccAlphaOnMsb = true; + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + Require(AgcDriver::GuestMemory::ImportWatched(metadata.Address(), BlockBytes, [&] { + auto* first = reinterpret_cast(metadata.data); + *first = *first; + return true; + }), "the import over resident multisampled DCC metadata failed"); + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + Require(AgcDriver::GuestMemory::ImportWatched(metadata.Address(), BlockBytes, [&] { + auto* first = reinterpret_cast(metadata.data); + *first = static_cast(*first ^ 0xffu); + return true; + }), "the second import over resident multisampled DCC metadata failed"); + bool refused = false; + 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("writes to a resident multisampled DCC") != std::string_view::npos; + } + Require(refused, "a store of new bytes to resident multisampled DCC metadata within an import window was not refused"); + programCount = 0; + } +} + int main(int argc, char** argv) { try { const auto device = OpenVulkanTestDevice(); @@ -828,6 +924,7 @@ int main(int argc, char** argv) { try { DccFastClearTests(*device, samples); DccRefillTests(*device, samples); + DccImportWindowTests(*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()); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp index 923b3b7212..a5395d5661 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp @@ -5,6 +5,7 @@ #include "VulkanTestDevice.hpp" #include #include +#include #include #include #include @@ -88,6 +89,10 @@ void CheckTable() { } int main() { +#ifdef ANYPS5_ENABLE_ASAN + std::puts("skipped: the shader calls through the FS table at the PS5's fixed 0xfe0040000, which an ANYPS5_ENABLE_ASAN build cannot map"); + return VulkanTestSkipped; +#endif try { { const auto device = OpenVulkanTestDevice(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DispatchDataHitWords.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchDataHitWords.cpp new file mode 100644 index 0000000000..c0d7cc48b9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchDataHitWords.cpp @@ -0,0 +1,152 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint32_t Lanes = 32; +constexpr std::uint32_t BufferBytes = Lanes * 4u; +constexpr std::uint32_t ComputeQueue = 0x20; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Unwritten = 0xcafebabeu; + +std::uint32_t* Constants = nullptr; +std::uint32_t* Input = nullptr; +std::uint32_t* Output = nullptr; + +alignas(256) constexpr std::array AddressCode{0xf4000200u, 0xfa000000u, 0x34020082u, 0xdc308000u, 0x03020001u, 0xbf8cc07fu, 0x7e040208u, 0x4a040481u, 0xbf8c3f70u, 0x4a040503u, 0xe0701000u, 0x80010201u, 0xbf810000u}; + +std::uint32_t* NewBlock() { +#ifdef _WIN32 + auto* block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + if (block == nullptr) throw std::runtime_error("dispatch data hit words: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + return block; +} + +void check(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("dispatch data hit words: " + what); +} + +template +void WaitUntil(Ready ready, const std::string& what) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (!ready()) { + if (std::chrono::steady_clock::now() >= deadline) throw std::runtime_error("dispatch data hit words: timed out waiting for " + what); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +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::vector Dispatch() { + const auto codeAddress = reinterpret_cast(AddressCode.data()); + const auto constants = reinterpret_cast(Constants); + const auto input = reinterpret_cast(Input); + const auto output = BufferDescriptor(Output, BufferBytes); + return { + 0xc0027600u, 0x20cu, static_cast(codeAddress >> 8u), static_cast(codeAddress >> 40u), + 0xc0037600u, 0x207u, Lanes, 1u, 1u, + 0xc0017600u, 0x213u, 8u << 1u, + 0xc0087600u, 0x240u, static_cast(constants), static_cast(constants >> 32u), static_cast(input), static_cast(input >> 32u), output[0], output[1], output[2], output[3], + 0xc0031500u, 1u, 1u, 1u, 0x8041u, + }; +} + +void SetConstant(std::uint32_t value) { + for (std::uint32_t lane = 0; lane < Lanes; ++lane) Constants[lane] = value; + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Constants), BufferBytes); +} + +void CheckOutput(std::uint32_t expected, const std::string& what) { + WaitUntil([] { + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output), BufferBytes); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + if (std::atomic_ref(Output[lane]).load(std::memory_order_acquire) == Unwritten) return false; + } + return true; + }, what + " to store"); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) check(Output[lane] == expected, what + ": lane " + std::to_string(lane) + " is " + std::to_string(Output[lane]) + ", expected " + std::to_string(expected)); +} + +void Round(std::uint32_t value, const std::string& what) { + SetConstant(value); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) Output[lane] = Unwritten; + auto words = Dispatch(); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + check(sceAgcDriverSubmitAcb(ComputeQueue, &packet) == 0, "submission failed"); + AgcDriverWaitIdle_nid_postfix(); + CheckOutput(value + 1u, what); +} + +} + +int main() { + try { + { + const auto probe = OpenVulkanTestDevice(); + if (!probe) return VulkanTestSkipped; + } + + Shader shader{}; + shader.file_header = 0x34333231; + shader.version = 0x18; + shader.header_size = sizeof(Shader); + shader.shader_size = sizeof(AddressCode); + shader.code = AddressCode.data(); + AgcDriverRegisterShader_nid_postfix(&shader); + + Constants = NewBlock(); + Input = NewBlock(); + Output = NewBlock(); + + Round(0x11112222u, "the first dispatch"); + const std::array values{0x33334444u, 0x55556666u, 0x7a7b7c7du, 0x0f1e2d3cu}; + for (std::size_t round = 0; round < values.size(); ++round) { + const auto mismatches = AgcDriver::Graphics::SnapshotMismatches(); + const auto what = "data hit " + std::to_string(round + 1) + ", resolved ahead on the compute queue"; + Round(values[round], what); + const auto added = AgcDriver::Graphics::SnapshotMismatches() - mismatches; + check(added == 0, what + ": its snapshots differ from the registered memory serving them (" + std::to_string(added) + " snapshot mismatches)"); + } + std::printf("dispatch data hit words: %zu data hits resolved ahead read their own live words\n", values.size()); + + LibcRunShutdown_nid_postfix(); + std::puts("dispatch data hit words tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { LibcRunShutdown_nid_postfix(); } catch (...) {} + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp index d78479f1c3..d45a4cc045 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp @@ -82,6 +82,11 @@ int main() { Require(Contains(ranges, written, written + surfaceBytes), "a written image next to a null one is not a write range"); Require(!Touches(ranges, 0, surfaceBytes), "a null storage image is a write range"); } + bool bounded = false; + const auto dispatchRanges = AgcDriver::DriverDetail::DispatchWriteRanges(program, bounded); + Require(bounded, "a dispatch whose written images are a real one and a null one is not bounded"); + Require(Contains(dispatchRanges, written, written + surfaceBytes), "a written image next to a null one is not a dispatch write range"); + Require(!Touches(dispatchRanges, 0, surfaceBytes), "a null storage image is a dispatch write range"); AgcDriver::Graphics::State targets{}; targets.hasColorTarget = true; auto& color = targets.color; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FailedCaptureDump.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FailedCaptureDump.cpp new file mode 100644 index 0000000000..0382270371 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FailedCaptureDump.cpp @@ -0,0 +1,143 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "ControlFlow/RequestSerializer.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +alignas(256) constexpr std::array Code{ + 0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e020280u, 0xe0702000u, 0x80020100u, 0xbf810000u, 0u, +}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +alignas(256) std::array Output{}; +alignas(16) std::array Valid{}; +alignas(16) std::array Unsupported{}; + +std::array BufferDescriptor(std::uint32_t word3) { + const auto address = reinterpret_cast(Output.data()); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), static_cast(Output.size()), word3}; +} + +std::array Pointer(const void* descriptor) { + const auto address = reinterpret_cast(descriptor); + return {static_cast(address), static_cast(address >> 32u)}; +} + +void Dispatch(const std::array& userData) { + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{64, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(Code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{static_cast(userData.size()) << 1u}; + registers(0x213, resource); + registers(0x240, userData); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); +} + +std::string Read(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + Require(file.is_open(), "shader request " + path.string() + " was not written"); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +std::string Replay(const std::string& text) { + const auto replay = ShaderRecompiler::RequestSerializer{}.Deserialize(text); + try { + static_cast(ShaderRecompiler::Recompile(replay.request)); + } catch (const std::exception& error) { + return error.what(); + } + return {}; +} + +std::vector UserData(const std::string& text) { + const auto replay = ShaderRecompiler::RequestSerializer{}.Deserialize(text); + return {replay.request.context.userData.begin(), replay.request.context.userData.end()}; +} + +void Run() { + char prefix[64]; + std::snprintf(prefix, sizeof(prefix), "shader_%llx", static_cast(reinterpret_cast(Code.data()))); + const std::string first = std::string(prefix) + ".req"; + const std::string failed = std::string(prefix) + "_failed.req"; + Valid = BufferDescriptor(0x00014204u); + Unsupported = BufferDescriptor(0xa1b00facu); + Output.fill(Untouched); + const auto valid = Pointer(Valid.data()); + Dispatch(valid); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); + for (const auto word : Output) Require(word == 0u, "the dispatch through the valid descriptor stored nothing"); + const auto unsupported = Pointer(Unsupported.data()); + std::string message; + try { + Dispatch(unsupported); + } catch (const std::exception& error) { + message = error.what(); + } + Require(message.find("unsupported type") != std::string::npos, "the dispatch through a type 2 descriptor failed for another reason: " + message); + Require(message.find("(" + failed + ")") != std::string::npos, "the failure does not name the failing dispatch's request: " + message); + const auto firstText = Read(first); + const auto failedText = Read(failed); + Require(UserData(firstText) == std::vector(valid.begin(), valid.end()), "the first dispatch's request lost its user data"); + Require(UserData(failedText) == std::vector(unsupported.begin(), unsupported.end()), "the failed request holds another dispatch's user data"); + Require(Replay(firstText).empty(), "the first dispatch's request does not replay: " + Replay(firstText)); + const auto replayed = Replay(failedText); + Require(replayed.find("unsupported type") != std::string::npos, "the failed request does not replay its failure: " + replayed); +} + +} + +int main() { + const auto directory = std::filesystem::temp_directory_path() / ("agc_failed_capture_dump-" + std::to_string(std::random_device{}())); + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + std::filesystem::remove_all(directory); + std::filesystem::create_directories(directory); + std::filesystem::current_path(directory); +#ifdef _WIN32 + Require(_putenv_s("APS5_DUMP_SHADERS", "1") == 0, "cannot enable shader capture"); +#else + Require(setenv("APS5_DUMP_SHADERS", "1", 1) == 0, "cannot enable shader capture"); +#endif + Run(); + try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {} + std::filesystem::current_path(directory.parent_path()); + std::filesystem::remove_all(directory); + std::puts("failed capture dump tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {} + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FrontFaceAllBits.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FrontFaceAllBits.cpp index d3f9b23635..409e71f12c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FrontFaceAllBits.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FrontFaceAllBits.cpp @@ -67,7 +67,7 @@ void Draw(AgcDriver::VulkanDevice& device, bool allBits) { pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::FrontFace); pixel.frontFace = true; pixel.frontFaceAllBits = allBits; - pixel.targetOutputMode[0] = 1; + pixel.targetOutputMode[0] = 7; pixel.targetExportMapping.fill(0xe4u); const std::vector pixelUserData(8, 0u); const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; @@ -87,6 +87,7 @@ void Draw(AgcDriver::VulkanDevice& device, bool allBits) { 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_R32_UINT, sizeof(Pixels), 0xe4u}; + state.color.uintExport = true; state.colors = {state.color}; state.hasColorTarget = true; state.renderExtent = {Width, Height}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp new file mode 100644 index 0000000000..0d29ae08e4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp @@ -0,0 +1,147 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Width = 8; +constexpr std::uint32_t Layers = 3; +constexpr std::uint32_t BaseArray = 8100; +constexpr std::uint32_t LastArray = BaseArray + Layers - 1u; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type1DArray = 12; +constexpr std::uint32_t ClampEdge = 2; + +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +alignas(65536) std::array Texels{}; + +alignas(256) constexpr std::array Code{ + 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf09c0f20, 0x00820602, 0xf0001f20, 0x00020a04, + 0x34020085, 0xbf8c3f70, 0xe0781000, 0x80010601, 0xe0781010, 0x80010a01, 0xbf810000, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(std::uint32_t baseArray = BaseArray) { + const auto address = static_cast(reinterpret_cast(Texels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Width - 1u) & 3u) << 30u), + (Width - 1u) >> 2u, + 0xfacu | (Type1DArray << 28u), + LastArray | (baseArray << 16u), 0u, 0u, 0u, + }; +} + +std::array SamplerDescriptor() { + return {ClampEdge | (ClampEdge << 3u) | (ClampEdge << 6u), 0u, 0u, 0u}; +} + +std::array Texel(std::uint32_t x, std::uint32_t guestLayer) { + return {static_cast((guestLayer * Width + x) * 9u + 3u), static_cast(x * 30u + guestLayer % 7u), static_cast(guestLayer), 0xff}; +} + +void FillTexels() { + Texels.fill(0); + const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor())); + const auto& mip = surface.mips.at(0); + Require(surface.layers == LastArray + 1u && surface.guestBytes <= Texels.size(), "the test array does not fit its storage"); + for (std::uint32_t layer = 0; layer <= LastArray; ++layer) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto texel = Texel(x, layer); + std::copy(texel.begin(), texel.end(), Texels.begin() + static_cast(surface.GuestLayerOffset(layer) + mip.tiledOffset + x * 4u)); + } + } +} + +void FillInput() { + constexpr std::array layers{0.0f, 1.0f, 2.0f, 0.4f, 1.6f, 2.4f, -1.0f, 5.0f}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * 4u + 0u] = std::bit_cast(-0.2f + 0.045f * static_cast(tid)); + Input[tid * 4u + 1u] = std::bit_cast(layers[tid % layers.size()]); + Input[tid * 4u + 2u] = tid % Width; + Input[tid * 4u + 3u] = (tid / Width) % Layers; + } +} + +void Expect(std::uint32_t tid, const char* name, const float* actual, const std::array& expected) { + for (std::uint32_t component = 0; component < 4u; ++component) { + const float scaled = actual[component] * 255.0f; + Require(std::fabs(scaled - std::round(scaled)) < 1e-3f && std::lround(scaled) == expected[component], std::string(name) + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(scaled) + "/255, expected " + std::to_string(expected[component]) + "/255"); + } +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t baseArray = BaseArray) { + Output.fill(-1.0f); + std::vector userData(20, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); + const auto texture = TextureDescriptor(baseArray); + const auto sampler = SamplerDescriptor(); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const std::span code(Code); + const std::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(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float u = std::bit_cast(Input[tid * 4u + 0u]); + const float layer = std::clamp(std::nearbyint(std::bit_cast(Input[tid * 4u + 1u])), 0.0f, static_cast(Layers - 1u)); + const auto x = static_cast(std::clamp(static_cast(std::floor(u * static_cast(Width))), 0, static_cast(Width) - 1)); + Expect(tid, "image_sample_lz on a window of a 1D array past maxImageArrayLayers", &Output[tid * 8u], Texel(x, baseArray + static_cast(layer))); + Expect(tid, "image_load on a window of a 1D array past maxImageArrayLayers", &Output[tid * 8u + 4u], Texel(Input[tid * 4u + 2u], baseArray + Input[tid * 4u + 3u])); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillTexels(); + FillInput(); + Run(*device); + try { + Run(*device, 0); + } catch (const std::exception& error) { + if (std::string(error.what()).find("an image view of more array slices than maxImageArrayLayers") == std::string::npos) throw; + } + std::puts("a window of a 1D array past maxImageArrayLayers was sampled and loaded from its viewed layers"); + 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 cf78250330..d353e82b93 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -873,7 +873,7 @@ void RunTests(AgcDriver::VulkanDevice& device) { const auto sintControl = RunDirect(device, LinearWrap, sintImage); const auto floatControl = RunDirect(device, LinearWrap, floatImage); const auto uintControl = RunDirect(device, LinearWrap, uintImage); - Require(floatControl.samplers == std::vector{LinearWrap} && uintControl.samplers == std::vector{LinearWrap} && sintControl.samplers == std::vector{PointWrap}, "image table: the direct bindings under a linear S# changed their sampler copies"); + Require(floatControl.samplers == std::vector{LinearWrap} && uintControl.samplers == std::vector{PointWrap} && sintControl.samplers == std::vector{PointWrap}, "image table: the direct bindings under a linear S# changed their sampler copies"); Require(Group(integerLinear, 0) == Group(sintControl, 0) && Group(integerLinear, 1) == Group(floatControl, 0), "image table: an entry under a linear direct S# differs from the direct binding"); RequireRed(integerLinear, 0, Lanes, [](std::uint32_t x) { return static_cast(SintTexels[x]); }, "sint texels through a linear direct sampler"); RequireRed(integerLinear, 2, Lanes, [](std::uint32_t x) { return UintTexels[x]; }, "uint texels through a linear direct sampler"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp index 3e16e1068a..765947dc99 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp @@ -358,6 +358,15 @@ bool WindowsImportsStayWatchedThroughReads(const AgcDriver::Graphics::Context& c } return false; } +#else +bool ImportsStayWatchedThroughReads(const AgcDriver::Graphics::Context& context) { + if (context.hostImportAlignment == 0) return true; + const auto probe = AgcDriver::Graphics::ProbeImportWriteProtection(context); + if (probe.failure == nullptr && probe.writtenAfterSubmit == 0) return true; + if (probe.failure != nullptr) std::printf("skipped, the device reports no host import probe result: it failed at %s (%d)\n", probe.failure, static_cast(probe.result)); + else std::printf("skipped, the device reports %u of %u host-imported pages written after a GPU read, so a watched imported table cannot be vouched for\n", probe.writtenAfterSubmit, probe.pages); + return false; +} #endif } @@ -371,6 +380,8 @@ int main(int argc, char** argv) { #ifdef _WIN32 if (argc == 2 && std::string(argv[1]) == "--vouched-lookups" && !WindowsImportsStayWatchedThroughReads(device->graphicsContext())) return VulkanTestSkipped; +#else + if (argc == 2 && std::string(argv[1]) == "--vouched-lookups" && !ImportsStayWatchedThroughReads(device->graphicsContext())) return VulkanTestSkipped; #endif } GuestBlock block; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/IndirectArgumentsPendingProducer.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectArgumentsPendingProducer.cpp index d1cc6ec8e0..306f35cbd0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/IndirectArgumentsPendingProducer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectArgumentsPendingProducer.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -168,12 +169,14 @@ public: Require(library != nullptr, "cannot load Vulkan for the producer submission gate"); resolve = reinterpret_cast(SDL_LoadFunction(library.get(), "vkGetDeviceProcAddr")); Require(resolve != nullptr, "cannot resolve vkGetDeviceProcAddr for the producer submission gate"); - const auto createEvent = function("vkCreateEvent"); - destroyEvent = function("vkDestroyEvent"); - setEvent = function("vkSetEvent"); - waitEvents = function("vkCmdWaitEvents"); - const VkEventCreateInfo info{VK_STRUCTURE_TYPE_EVENT_CREATE_INFO}; - Require(createEvent(device.Device(), &info, nullptr, &event) == VK_SUCCESS, "cannot create the producer submission gate"); + const auto createSemaphore = function("vkCreateSemaphore"); + destroySemaphore = function("vkDestroySemaphore"); + signalSemaphore = reinterpret_cast(resolve(device.Device(), "vkSignalSemaphoreKHR")); + if (signalSemaphore == nullptr) signalSemaphore = function("vkSignalSemaphore"); + VkSemaphoreTypeCreateInfo type{VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO}; + type.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE; + VkSemaphoreCreateInfo info{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, &type}; + Require(createSemaphore(device.Device(), &info, nullptr, &semaphore) == VK_SUCCESS, "cannot create the producer submission gate"); } ~ProducerSubmissionGate() { @@ -181,16 +184,17 @@ public: worker.request_stop(); worker.join(); } - setEvent(device.Device(), event); + Release(); try { device.WaitIdle(); } catch (...) { } - destroyEvent(device.Device(), event, nullptr); + destroySemaphore(device.Device(), semaphore, nullptr); } - void Record(AgcDriver::Graphics::Recorder& recorder) { - waitEvents(recorder.Commands(), 1, &event, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, nullptr, 0, nullptr, 0, nullptr); + void Arm(AgcDriver::Graphics::Recorder& recorder) { + recorder.Commands(); + recorder.HoldNextSubmit(semaphore, 1); } void ReleaseAfterSubmission(AgcDriver::Graphics::Recorder& recorder, std::uint64_t serial) { @@ -198,8 +202,8 @@ public: const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); while (!stop.stop_requested() && recorder.NewestSubmitted() < serial && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::microseconds(100)); producerSubmitted = recorder.NewestSubmitted() >= serial; - releaseResult = setEvent(device.Device(), event); - if (!producerSubmitted && !stop.stop_requested()) std::fprintf(stderr, "pending producer gate: watchdog expired with producer batch %llu unsubmitted (vkSetEvent %d)\n", static_cast(serial), static_cast(releaseResult)); + Release(); + if (!producerSubmitted && !stop.stop_requested()) std::fprintf(stderr, "pending producer gate: watchdog expired with producer batch %llu unsubmitted (vkSignalSemaphore %d)\n", static_cast(serial), static_cast(releaseResult)); }); } @@ -218,14 +222,22 @@ private: return function; } + void Release() { + if (signaled.exchange(true)) return; + VkSemaphoreSignalInfo info{VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO}; + info.semaphore = semaphore; + info.value = 1; + releaseResult = signalSemaphore(device.Device(), &info); + } + AgcDriver::VulkanDevice& device; std::unique_ptr library{nullptr, &SDL_UnloadObject}; PFN_vkGetDeviceProcAddr resolve = nullptr; - PFN_vkDestroyEvent destroyEvent = nullptr; - PFN_vkSetEvent setEvent = nullptr; - PFN_vkCmdWaitEvents waitEvents = nullptr; - VkEvent event = VK_NULL_HANDLE; + PFN_vkDestroySemaphore destroySemaphore = nullptr; + PFN_vkSignalSemaphore signalSemaphore = nullptr; + VkSemaphore semaphore = VK_NULL_HANDLE; std::jthread worker; + std::atomic signaled{false}; bool producerSubmitted = false; VkResult releaseResult = VK_NOT_READY; }; @@ -243,7 +255,7 @@ void RunArgumentLabel(Slot& slot) { auto* recorder = AgcDriver::Graphics::Recorder::Active(); Require(recorder != nullptr, "the argument-label test has no active recorder"); ProducerSubmissionGate gate(*device); - gate.Record(*recorder); + gate.Arm(*recorder); const std::uint32_t labelValue = 2; const auto labelAddress = reinterpret_cast(&slot.arguments[1]); Require(device->WriteLabelOnGpu(labelAddress, std::as_bytes(std::span(&labelValue, 1)), 1, Graphics, false) == 0, "the argument label was not recorded on the GPU"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp index 210d8e681d..8ec6a58bd2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp @@ -24,6 +24,13 @@ alignas(256) constexpr std::array ScanCode{ 0xe0701000, 0x80030802, 0xbf810000, }; +constexpr auto StoreScanCode = [] { + auto code = ScanCode; + code[16] = 0xe0745000; + code[38] = 0xe0745000; + return code; +}(); + constexpr std::uint32_t Lanes = 64; constexpr std::uint32_t Blocks = 4096; @@ -41,9 +48,17 @@ std::array BufferDescriptor(const void* data, std::uint32_t by return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; } -void Run(AgcDriver::VulkanDevice& device, std::uint32_t groups) { - alignas(256) static std::array code{}; - code = ScanCode; +struct Variant { + const char* name; + const std::array& words; +}; + +constexpr std::array Variants{{{"buffer_atomic_swap_x2", ScanCode}, {"buffer_store_dwordx2", StoreScanCode}}}; + +void Run(AgcDriver::VulkanDevice& device, std::size_t variant, std::uint32_t groups) { + alignas(256) static std::array, Variants.size()> programs{}; + auto& code = programs[variant]; + code = Variants[variant].words; std::vector userData; for (const auto& descriptor : {BufferDescriptor(Counter.data(), sizeof(Counter)), BufferDescriptor(Aggregates.data(), sizeof(Aggregates)), BufferDescriptor(Pairs.data(), sizeof(Pairs)), BufferDescriptor(Prefixes.data(), sizeof(Prefixes))}) { userData.insert(userData.end(), descriptor.begin(), descriptor.end()); @@ -64,8 +79,8 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t groups) { device.WaitIdle(); } -void CheckLookBack(AgcDriver::VulkanDevice& device, std::uint32_t block) { - const auto name = "look-back from block " + std::to_string(block); +void CheckLookBack(AgcDriver::VulkanDevice& device, std::size_t variant, std::uint32_t block) { + const auto name = std::string(Variants[variant].name) + " look-back from block " + std::to_string(block); Pairs.fill(0u); std::uint32_t prefix = 0; for (std::uint32_t b = 0; b < Blocks; ++b) Aggregates[b] = Value(b, block); @@ -77,20 +92,20 @@ void CheckLookBack(AgcDriver::VulkanDevice& device, std::uint32_t block) { Counter.fill(0u); Counter[0] = block; Prefixes.fill(0xdeadbeefu); - Run(device, 1); + Run(device, variant, 1); Require(Counter[0] == block + 1, name + ": the counter is " + std::to_string(Counter[0])); Require(Prefixes[block] == prefix, name + ": its prefix is " + std::to_string(Prefixes[block]) + ", expected " + std::to_string(prefix)); Require(Pairs[2 * block] == 2u, name + ": its flag is " + std::to_string(Pairs[2 * block]) + ", expected 2"); Require(Pairs[2 * block + 1] == prefix + Aggregates[block], name + ": it published " + std::to_string(Pairs[2 * block + 1]) + ", expected " + std::to_string(prefix + Aggregates[block])); } -void CheckWhole(AgcDriver::VulkanDevice& device, std::uint32_t seed) { - const auto name = "whole scan " + std::to_string(seed); +void CheckWhole(AgcDriver::VulkanDevice& device, std::size_t variant, std::uint32_t seed) { + const auto name = std::string(Variants[variant].name) + " whole scan " + std::to_string(seed); for (std::uint32_t b = 0; b < Blocks; ++b) Aggregates[b] = Value(b, seed); Counter.fill(0u); Pairs.fill(0u); Prefixes.fill(0xdeadbeefu); - Run(device, Blocks); + Run(device, variant, Blocks); std::uint32_t running = 0; for (std::uint32_t b = 0; b < Blocks; ++b) { Require(Prefixes[b] == running, name + ": block " + std::to_string(b) + "'s prefix is " + std::to_string(Prefixes[b]) + ", expected " + std::to_string(running)); @@ -113,8 +128,10 @@ int main() { std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); return VulkanTestSkipped; } - for (const std::uint32_t block : {0u, 1u, 2u, 63u, 64u, 1000u, Blocks - 1}) CheckLookBack(*device, block); - for (std::uint32_t seed = 0; seed < 50; ++seed) CheckWhole(*device, seed); + for (std::size_t variant = 0; variant < Variants.size(); ++variant) { + for (const std::uint32_t block : {0u, 1u, 2u, 63u, 64u, 1000u, Blocks - 1}) CheckLookBack(*device, variant, block); + for (std::uint32_t seed = 0; seed < 50; ++seed) CheckWhole(*device, variant, seed); + } std::puts("look-back scan tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/QueuedRawShader.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedRawShader.cpp new file mode 100644 index 0000000000..703f7523d7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedRawShader.cpp @@ -0,0 +1,66 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void QueuedCode(bool cached) { + using namespace AgcDriver::DriverDetail; + alignas(256) static std::array, 2> programs{}; + auto& code = programs[cached ? 0 : 1]; + code.fill(0xbf810000u); + code[0] = 0x7e000200u; + const auto address = reinterpret_cast(code.data()); + const auto before = cached ? ReadRawShader(address) : nullptr; + std::atomic finished{false}; + std::atomic started{false}; + struct Restore { + std::atomic& finished; + bool active = DrawPipeline::Active(); + ~Restore() { + finished.store(true, std::memory_order_release); + DrawPipeline::Active() = active; + DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Submission); + } + } restore{finished}; + DrawPipeline::Queue0().Enqueue([&, address] { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(200); + started.store(true, std::memory_order_release); + while (!finished.load(std::memory_order_acquire) && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + const std::uint32_t replacement = 0x7e0002f2u; + AgcDriver::GuestMemory::Write(address, std::as_bytes(std::span(&replacement, 1)), alignof(std::uint32_t)); + }, {{address, address + sizeof(std::uint32_t)}}); + while (!started.load(std::memory_order_acquire)) std::this_thread::yield(); + DrawPipeline::Active() = true; + const auto after = ReadRawShader(address); + finished.store(true, std::memory_order_release); + DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Submission); + if (after->code.front() != 0x7e0002f2u || (before != nullptr && (before->code.front() != 0x7e000200u || before == after))) { + throw std::runtime_error(std::string(cached ? "cached" : "fresh") + " raw shader read ignored a queued write to its code"); + } +} + +} + +int main(int argc, char** argv) { + try { + if (AgcDriver::DriverDetail::DrawPipeline::Depth() == 0) return 77; + if (argc == 1 || std::string(argv[1]) == "cached") QueuedCode(true); + if (argc == 1 || std::string(argv[1]) == "fresh") QueuedCode(false); + std::cout << "Raw shader reads observed queued code writes\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 881867d747..e96d71cc14 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -17,7 +17,9 @@ #endif #include #endif +#include #include +#include #include #include #include @@ -26,8 +28,11 @@ #include #include #include +#include +#include #include #include +#include #include #include #include @@ -48,6 +53,67 @@ constexpr std::size_t AllocationBytes = BlockBytes + RegistrationGapBytes; constexpr std::uint64_t Budget = 1ull << 20u; constexpr std::uint64_t Ceiling = Budget + Budget / 4u; +std::optional selectDevice(std::span devices, std::string_view request) { + const auto rankDeviceType = [](VkPhysicalDeviceType type) { + switch (type) { + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 3; + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 2; + case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 1; + default: return 0; + } + }; + std::optional selected; + int selectedRank = -1; + for (std::size_t index = 0; index < devices.size(); ++index) { + const auto& candidate = devices[index]; + if (candidate.apiVersion < VK_API_VERSION_1_1) continue; + const std::string_view name(candidate.deviceName); + if (!request.empty() && std::search(name.begin(), name.end(), request.begin(), request.end(), [](char left, char right) { + return std::tolower(static_cast(left)) == std::tolower(static_cast(right)); + }) == name.end()) continue; + const int rank = request.empty() ? rankDeviceType(candidate.deviceType) : 4; + if (rank <= selectedRank) continue; + selected = index; + selectedRank = rank; + } + return selected; +} + +void deviceSelectionTests() { + const auto properties = [](const char* name, VkPhysicalDeviceType type) { + VkPhysicalDeviceProperties result{}; + result.apiVersion = VK_API_VERSION_1_1; + result.deviceType = type; + std::memcpy(result.deviceName, name, std::strlen(name) + 1u); + return result; + }; + std::array devices{ + properties("AMD Radeon Graphics", VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU), + properties("NVIDIA GeForce RTX 5080", VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU), + properties("Virtual GPU", VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU), + properties("CPU", VK_PHYSICAL_DEVICE_TYPE_CPU), + }; + Require(!selectDevice({}, {}).has_value(), "an empty device list selected a device"); + Require(selectDevice(devices, {}) == 1u, "default selection did not prefer the discrete GPU"); + Require(selectDevice(devices, "AMD") == 0u, "the AMD request did not select the integrated GPU"); + Require(selectDevice(devices, "NVIDIA") == 1u, "the NVIDIA request did not select the discrete GPU"); + Require(selectDevice(devices, "aMd rAdEoN") == 0u, "a requested integrated GPU lost to an unrelated discrete GPU"); + Require(selectDevice(devices, "rtx 5080") == 1u, "a partial GPU name did not match without regard to case"); + Require(selectDevice(devices, "Virtual") == 2u, "a requested virtual GPU was not selected"); + Require(selectDevice(devices, "cpu") == 3u, "a requested CPU device was not selected"); + Require(!selectDevice(devices, "missing").has_value(), "an unmatched request selected another GPU"); + Require(!selectDevice(devices, "NVIDIA GeForce RTX 5080 with a longer name").has_value(), "a request longer than the device name matched"); + Require(selectDevice(std::span(devices).subspan(2), {}) == 0u, "default selection did not prefer the virtual GPU to the CPU"); + devices[1].apiVersion = VK_API_VERSION_1_0; + Require(selectDevice(devices, {}) == 0u, "default selection did not prefer the integrated GPU to the virtual GPU"); + Require(!selectDevice(devices, "NVIDIA").has_value(), "a requested device without Vulkan 1.1 was selected"); + devices[1] = devices[0]; + Require(selectDevice(devices, {}) == 0u && selectDevice(devices, "AMD") == 0u, "equally ranked matches did not retain enumeration order"); + for (auto& device : devices) device.apiVersion = VK_API_VERSION_1_0; + Require(!selectDevice(devices, {}).has_value(), "a list without Vulkan 1.1 selected a device"); + std::puts("sampled-cache device selection CPU tests passed"); +} + class Device { public: explicit Device(bool enableStorageImageMultisample = true) { @@ -71,7 +137,13 @@ public: Require(count != 0, "no Vulkan device"); std::vector devices(count); Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); - context.physical = devices.front(); + const auto physicalProperties = function("vkGetPhysicalDeviceProperties"); + std::vector deviceProperties(devices.size()); + for (std::size_t index = 0; index < devices.size(); ++index) physicalProperties(devices[index], &deviceProperties[index]); + const char* requestedName = std::getenv("ANYPS5_GPU"); + const auto selectedIndex = selectDevice(deviceProperties, requestedName != nullptr ? requestedName : ""); + Require(selectedIndex.has_value(), requestedName != nullptr && *requestedName != '\0' ? std::string("no Vulkan 1.1 device matching ANYPS5_GPU=\"") + requestedName + "\"" : "no Vulkan 1.1 device"); + context.physical = devices[*selectedIndex]; const auto extensions = function("vkEnumerateDeviceExtensionProperties"); Check(extensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); std::vector available(count); @@ -122,6 +194,7 @@ public: function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); VkPhysicalDeviceProperties properties{}; function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + std::printf("sampled-cache fixture Vulkan device: %s vendor=0x%04x device=0x%04x\n", properties.deviceName, properties.vendorID, properties.deviceID); context.limits = properties.limits; context.bufferDeviceAddress = true; context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); @@ -141,6 +214,14 @@ public: ~Device() { release(); } const Context& GetContext() const { return context; } + void VerifySelection() const { + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + const char* request = std::getenv("ANYPS5_GPU"); + Require(selectDevice(std::span(&properties, 1), request != nullptr ? request : "").has_value(), "the sampled-cache fixture physical device does not match ANYPS5_GPU"); + std::puts("sampled-cache fixture physical device selection verified"); + } + private: template TFunction function(const char* name) const { @@ -563,10 +644,10 @@ void frameTests(const Context& context) { hits += again == made[surface].lock(); } Require(hits == capacity, "the next frame over the same " + std::to_string(frame) + " textures found " + std::to_string(hits) + " of them cached, not the " + std::to_string(capacity) + " the cache kept"); - auto admitted = Sampled(context, block.Surface(frame - 1u)); - Require(Sampled(context, block.Surface(frame - 1u)) == admitted, "a texture used a second time in the frame was not cached"); - Require(TextureCacheUsage().sampledEntries == capacity && TextureCacheUsage().sampledBytes <= Budget, "the texture admitted on its second use left the cache over its budget"); - admitted.reset(); + auto refused = Sampled(context, block.Surface(frame - 1u)); + Require(Sampled(context, block.Surface(frame - 1u)) != refused, "a texture used a second time in the frame was cached in place of one of the frame's textures"); + Require(TextureCacheUsage().sampledEntries == capacity && TextureCacheUsage().sampledBytes <= Budget, "the texture used a second time left the cache over its budget"); + refused.reset(); Recorder::CountPresent(); std::vector> moved(frame); for (std::size_t surface = 0; surface < frame; ++surface) moved[surface] = Sampled(context, block.Surface(frame + surface)); @@ -578,6 +659,71 @@ void frameTests(const Context& context) { Require(TextureCacheUsage().sampledEntries == capacity, "a second frame over other textures left " + std::to_string(TextureCacheUsage().sampledEntries) + " entries cached"); } +void scanTests(const Context& context) { + Block block(context); + ClearCachedTextures(context.device); + Recorder::CountPresent(); + const auto bytes = Sampled(context, block.Surface(0))->AllocationBytes(); + const auto capacity = static_cast(Budget / bytes); + const auto working = 2u * capacity; + Require(capacity >= 2u && working <= Surfaces && TextureCacheUsage().sampledEntries == 1u, "a snapshot of the 256 KiB surface holds " + std::to_string(bytes) + " bytes, which the test's 1 MiB budget does not fit as planned"); + constexpr std::size_t Presents = 6; + constexpr std::size_t Uses = 2; + std::vector> last(working); + std::size_t hits = 0; + std::size_t lookups = 0; + for (std::size_t present = 0; present < Presents; ++present) { + Recorder::CountPresent(); + for (std::size_t surface = 0; surface < working; ++surface) { + for (std::size_t use = 0; use < Uses; ++use) { + const auto texture = Sampled(context, block.Surface(surface)); + if (present != 0u) { + hits += texture == last[surface].lock(); + ++lookups; + } + last[surface] = texture; + } + } + } + Require(TextureCacheUsage().sampledBytes <= Budget, "the sampled texture cache holds more than its budget after a scan"); + const auto wanted = (Presents - 1u) * Uses * capacity / 2u; + Require(hits >= wanted, "a cyclic scan using each of " + std::to_string(working) + " textures twice in a row, twice the " + std::to_string(capacity) + " the budget holds, found " + std::to_string(hits) + " of " + std::to_string(lookups) + " lookups cached, fewer than " + std::to_string(wanted)); +} + +void shrinkTests(const Context& context) { + Block block(context); + ClearCachedTextures(context.device); + Recorder::CountPresent(); + OverrideCacheBudgetForTest(2u * Budget); + const auto bytes = Sampled(context, block.Surface(0))->AllocationBytes(); + const auto capacity = static_cast((2u * Budget) / bytes); + Require(capacity >= 4u && capacity + 1u <= Surfaces, "a snapshot of the 256 KiB surface holds " + std::to_string(bytes) + " bytes, which the test's 2 MiB budget does not fit as planned"); + for (std::size_t surface = 1; surface < capacity; ++surface) Sampled(context, block.Surface(surface)); + Require(TextureCacheUsage().sampledEntries == capacity, "the 2 MiB budget did not keep " + std::to_string(capacity) + " snapshots"); + Recorder::CountPresent(); + OverrideCacheBudgetForTest(Budget); + for (std::size_t surface = 0; surface < capacity; ++surface) Sampled(context, block.Surface(surface)); + Sampled(context, block.Surface(capacity)); + Require(TextureCacheUsage().sampledBytes <= Ceiling, "after the budget fell to 1 MiB, one insert left " + std::to_string(TextureCacheUsage().sampledBytes) + " bytes cached with every entry in use this frame, past a quarter over the budget"); + OverrideCacheBudgetForTest(0); +} + +void secondUseTests(const Context& context) { + Block block(context); + ClearCachedTextures(context.device); + Recorder::CountPresent(); + const auto bytes = Sampled(context, block.Surface(0))->AllocationBytes(); + const auto capacity = static_cast(Budget / bytes); + Require(capacity >= 2u && capacity < Surfaces && TextureCacheUsage().sampledEntries == 1u, "a snapshot of the 256 KiB surface holds " + std::to_string(bytes) + " bytes, which the test's 1 MiB budget does not fit as planned"); + for (std::size_t surface = 1; surface < capacity; ++surface) Sampled(context, block.Surface(surface)); + Recorder::CountPresent(); + auto first = Sampled(context, block.Surface(capacity)); + Require(TextureCacheUsage().sampledEntries == capacity && first.use_count() == 1, "the first use in a frame of a texture the cache does not hold was cached in place of one the last frame used"); + const auto second = Sampled(context, block.Surface(capacity)); + Require(Sampled(context, block.Surface(capacity)) == second, "a texture used a second time in a frame whose least recently used entry is from the last frame was not cached"); + Require(TextureCacheUsage().sampledEntries == capacity && TextureCacheUsage().sampledBytes <= Budget, "the texture admitted on its second use left the cache over its budget"); +} + void earlyMissTests(const Context& context) { Block block(context); ClearCachedTextures(context.device); @@ -718,6 +864,10 @@ int main(int argc, char** argv) { try { const bool memory = argc > 1 && std::string(argv[1]) == "memory"; const std::string mode = argc > 1 ? argv[1] : ""; + if (mode == "device-selection-cpu") { + deviceSelectionTests(); + return 0; + } const bool resources = mode == "resource-admission" || mode == "resource-oversized" || mode == "resource-clear"; const bool disableStorageImageMultisample = mode == "without-storage-image-multisample"; #ifdef _WIN32 @@ -729,10 +879,15 @@ int main(int argc, char** argv) { try { device = std::make_unique(!disableStorageImageMultisample); } catch (const std::exception& error) { - if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + const char* requestedName = std::getenv("ANYPS5_GPU"); + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr || (requestedName != nullptr && *requestedName != '\0')) throw; std::printf("skipped, no usable Vulkan device: %s\n", error.what()); return VulkanTestSkipped; } + if (mode == "device-selection") { + device->VerifySelection(); + return 0; + } auto context = device->GetContext(); if (disableStorageImageMultisample) { PlaceholderImages placeholders(context); @@ -774,7 +929,10 @@ int main(int argc, char** argv) { rescanTests(context); viewEvictionTests(context); viewTests(context); + scanTests(context); frameTests(context); + shrinkTests(context); + secondUseTests(context); earlyMissTests(context); collidingKeyTests(context); ClearCachedTextures(context.device); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSetpc.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSetpc.cpp index 669e125986..41069b2013 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSetpc.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSetpc.cpp @@ -274,6 +274,22 @@ void Run(AgcDriver::VulkanDevice& device, std::span code, s device.WaitIdle(); } +std::vector Scheduled(std::span original) { + std::vector code(original.begin(), original.end()); + code.insert(code.begin() + 25, 0x7e3c0280u); + code.insert(code.begin() + 24, 0x7e3e0280u); + const auto relocated = [](std::int32_t word) { return word + (word >= 24 ? 1 : 0) + (word >= 25 ? 1 : 0); }; + for (std::int32_t word = 0; word < static_cast(original.size()); ++word) { + const auto instruction = original[static_cast(word)]; + const auto opcode = instruction & 0xffff0000u; + if (opcode != 0xbf820000u && opcode != 0xbf850000u) continue; + const auto target = word + 1 + static_cast(instruction & 0xffffu); + const auto offset = relocated(target) - relocated(word) - 1; + code[static_cast(relocated(word))] = opcode | (static_cast(offset) & 0xffffu); + } + return code; +} + void CheckReversedSubtractRefused(AgcDriver::VulkanDevice& device) { std::string refusal; try { @@ -306,6 +322,14 @@ int main() { Check(Expected64); Run(*device, Wave64Code, 64, device->ComputeTarget(32)); Check(Expected64); + const auto scheduled32 = Scheduled(Wave32Code); + const auto scheduled64 = Scheduled(Wave64Code); + Run(*device, scheduled32, 32, device->ComputeTarget(32)); + Check(Expected32); + Run(*device, scheduled64, 64, device->Target()); + Check(Expected64); + Run(*device, scheduled64, 64, device->ComputeTarget(32)); + Check(Expected64); CheckReversedSubtractRefused(*device); std::puts("setpc tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp index d6a3055e6d..3612fae6ef 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp @@ -321,9 +321,10 @@ int main() { } Require(refused, "unsafe primitive routing was not refused"); } - bool unknownRefused = false; - try { Draw(*device, ColumnPixelCode, exported, replace()); } catch (const std::exception& error) { unknownRefused = std::string(error.what()).find("proven set") != std::string::npos; } - Require(unknownRefused, "unbounded stencil reference was not refused"); + Draw(*device, ColumnPixelCode, exported, replace()); + Check(all, "unbounded stencil export"); + Draw(*device, PlainPixelCode, plain, equal(5u)); + Check([](std::uint32_t x, std::uint32_t) { return x == 5u; }, "unbounded stencil export replaces each column's own value"); bool earlyRefused = false; try { Draw(*device, alternating, 0x1012u, replace()); } catch (const std::exception& error) { earlyRefused = std::string(error.what()).find("late tests") != std::string::npos; } Require(earlyRefused, "early stencil testing was not refused"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp index 4944237df7..bb378bac87 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "../SubvectorLoopFixture.hpp" #include "Recompiler.hpp" #include "VulkanTestDevice.hpp" #include @@ -17,6 +18,7 @@ namespace { using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; +using SubvectorLoopFixture::ExecKeptCode; constexpr std::uint32_t Threads = 64; constexpr std::uint32_t Columns = 9; @@ -25,16 +27,6 @@ constexpr std::uint32_t Marker = 0x5a5a5a5au; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array ExecKeptCode{ - 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, - 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000a, 0x80148114, 0x4a141481, - 0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850001, 0xbe26fff6, - 0xbe98047e, 0xbefe04c1, 0x7e1e0226, 0x7e200218, 0x7e220219, 0x7e240214, 0xe0701000, 0x80010a01, - 0xe0701100, 0x80010b01, 0xe0701200, 0x80010c01, 0xe0701300, 0x80010d01, 0xe0701400, 0x80010e01, - 0xe0701500, 0x80010f01, 0xe0701600, 0x80011001, 0xe0701700, 0x80011101, 0xe0701800, 0x80011201, - 0xbf810000, -}; - alignas(256) constexpr std::array ExecResetCode{ 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000b, 0x80148114, 0x4a141481, diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index e6c8fcbd75..c458568944 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -42,4 +42,29 @@ int APS5_VABI sceAjmDecWVorbisCreateSetupPacket(void) { return 0; } +int APS5_VABI sceAjmBatchGetLog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAjmInstanceAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +uint32_t APS5_VABI sceAjmInstanceCodecType(uint32_t instance_id) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAjmInstanceExtend(uint32_t context, uint32_t codec, uint64_t flags, uint32_t instance) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAjmInstanceSwitch(uint32_t context, uint32_t codec, uint64_t flags, uint32_t instance) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 6819db59e0..d6051d613f 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -18,7 +18,6 @@ extern "C" { #include #include #include -#include #include #include #include @@ -187,7 +186,7 @@ std::atomic g_nextContext{1}; std::atomic g_nextInstance{1}; std::atomic g_nextBatch{1}; std::mutex& g_batchLock = *new std::mutex(); -std::set& g_batches = *new std::set(); +std::map& g_batches = *new std::map(); enum class JobKind : std::uint32_t { Initialize = 1, @@ -1833,7 +1832,7 @@ int APS5_VABI sceAjmBatchJobGetStatistics(AjmBatchInfo* info, float interval, vo } int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int priority, AjmBatchError* error, uint32_t* batch) { - (void)context; + if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT; (void)priority; if (!info || !batch) return SCE_AJM_ERROR_INVALID_PARAMETER; AJM_TRACE("[ajm] batch start: context %u, priority %d, %llu of %llu buffer bytes used\n", context, priority, static_cast(info->offset), static_cast(info->size)); @@ -1863,18 +1862,20 @@ int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int p const auto id = g_nextBatch.fetch_add(1, std::memory_order_relaxed); { std::lock_guard lock(g_batchLock); - g_batches.insert(id); + g_batches.emplace(id, context); } *batch = id; return 0; } int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout, AjmBatchError* error) { - (void)context; + if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT; (void)timeout; { std::lock_guard lock(g_batchLock); - if (g_batches.erase(batch) == 0) return SCE_AJM_ERROR_INVALID_BATCH; + const auto found = g_batches.find(batch); + if (found == g_batches.end() || found->second != context) return SCE_AJM_ERROR_INVALID_BATCH; + g_batches.erase(found); } if (error) std::memset(error, 0, sizeof(*error)); return 0; @@ -1883,7 +1884,8 @@ int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout int APS5_VABI sceAjmBatchCancel(uint32_t context, uint32_t batch) { if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT; std::lock_guard lock(g_batchLock); - return g_batches.contains(batch) ? 0 : SCE_AJM_ERROR_INVALID_BATCH; + const auto found = g_batches.find(batch); + return found != g_batches.end() && found->second == context ? 0 : SCE_AJM_ERROR_INVALID_BATCH; } int APS5_VABI sceAjmBatchErrorDump(const AjmBatchInfo* info, AjmBatchError* error) { diff --git a/core/libs/prx/libSceAppChecker/Export.cpp b/core/libs/prx/libSceAppChecker/Export.cpp index d81f4a5e32..b50753644e 100644 --- a/core/libs/prx/libSceAppChecker/Export.cpp +++ b/core/libs/prx/libSceAppChecker/Export.cpp @@ -1,10 +1,23 @@ -#include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceAppCheckerAbort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppCheckerExecute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppCheckerExecuteEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index 20ec69a1e9..d7d94203b0 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -96,7 +96,7 @@ int APS5_VABI sceAppContentAppParamGetInt(uint32_t param_id, int32_t* value) { int APS5_VABI sceAppContentDownloadDataFormat(const AppContentMountPoint* mount_point) { if (!mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER; - if (std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX; + if (std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED; const auto directory = ResolvePath_nid_no_patch(DOWNLOAD_MOUNT_POINT); if (std::filesystem::create_directories(directory)) RecordWrittenPath_nid_no_patch(directory); ClearDirectory(directory); @@ -104,8 +104,8 @@ int APS5_VABI sceAppContentDownloadDataFormat(const AppContentMountPoint* mount_ } int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMountPoint* mount_point, size_t* available_space_kb) { - if (!available_space_kb) return SCE_APP_CONTENT_ERROR_PARAMETER; - if (!mount_point || std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX; + if (!mount_point || !available_space_kb) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED; const std::uint64_t quotaKb = GetAppDownloadDataSizeMiB_nid_postfix() * 1024u; if (quotaKb == 0) { *available_space_kb = 0; diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index c21557a76f..a5a107c931 100644 --- a/core/libs/prx/libSceAudioIn/Export.cpp +++ b/core/libs/prx/libSceAudioIn/Export.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace { @@ -196,16 +197,15 @@ int32_t APS5_VABI sceAudioInIsPadEmbeddedMic(int32_t handle) { } int32_t APS5_VABI sceAudioInGetGain(int32_t handle, float* gain) { - (void)handle; - (void)gain; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!gain) throw std::runtime_error("sceAudioInGetGain: null gain"); + *gain = 1.0f; + std::lock_guard lock(g_mutex); + return find(handle) ? 0 : AUDIO_IN_ERROR_INVALID_HANDLE; } int32_t APS5_VABI sceAudioInGetRerouteCount(int32_t handle) { - (void)handle; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + return find(handle) ? 0 : AUDIO_IN_ERROR_INVALID_HANDLE; } } diff --git a/core/libs/prx/libSceAudioLatencyEstimation/Export.cpp b/core/libs/prx/libSceAudioLatencyEstimation/Export.cpp index d81f4a5e32..71b2edeba4 100644 --- a/core/libs/prx/libSceAudioLatencyEstimation/Export.cpp +++ b/core/libs/prx/libSceAudioLatencyEstimation/Export.cpp @@ -1,10 +1,28 @@ -#include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceAudioLatencyEstimationAddData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioLatencyEstimationGetDelayTime() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioLatencyEstimationInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioLatencyEstimationRelease() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 59f2383f50..665bb433be 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -9,6 +9,7 @@ add_library(libSceAudioOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2User.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Mastering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2System.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Unimplemented.cpp ) target_include_directories(libSceAudioOut PRIVATE diff --git a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp new file mode 100644 index 0000000000..d87292b4c0 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp @@ -0,0 +1,488 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAudioOut2UserCreateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortSetAttributes(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextAdvance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextPush(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextGetQueueLevel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetSystemDebugState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortGetState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetHandleStatusInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetSimulatedHandleStatusInfo2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutChangeAppModuleState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetSimulatedHandleStatusInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetSimulatedBusUsableStatusByBusType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutAttachToApplicationByPid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutDetachFromApplicationByPid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutPtOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutPtClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutPtGetLastOutputTime(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExPtOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExPtClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExPtGetLastOutputTime(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExGetLastOutputTime(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetInfoOpenNum(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutMasteringInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutMasteringSetParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutMasteringGetState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutMasteringTerm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetPadSpkVolume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetPadJackVolume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetUsbVolume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetCaesarVolume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetHeadphoneOutMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetFocusEnablePid(void) { + NotImplemented_nid_no_patch(__func__); + 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; +} + +int APS5_VABI sceAudioOutSetChatBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetRecMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetAllMute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetSparkEQ(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutArbitrationInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2ArbitrationInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnable3DAudioByArbitration(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutAssignHRTF(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnable3DAudioForUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableMonoHpOutput(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableTVVirtual(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableTVCorrection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableVrControllerLowPowerMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableCinematicVrRotation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutCustomHrtfIsExist(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExConfigureOutputMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExSystemInfoIsSupportedAudioOutExMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExGetSystemInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExGetOutputInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysConfigureOutputMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysGetMonitorInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysHdmiMonitorInfoIsSupportedAudioOutMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysGetSystemInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysGetOutputInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextQueryMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextSetAttributes(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetPortConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetPortStatuses(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetHeadphoneOutMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusUserLoginFor3DAudio(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetDeviceConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetMorpheusParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetCaesarParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetSparkParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2AllAudioMainToHeadphoneForDiag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortGetStateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2ReportRegisterHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2ReportUnregisterHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetTvCorrectionInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetIpcHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetSystemStateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MasteringInitEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2SetDebugHeadphoneMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2DebugStateCtrl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2ContextBedWrite(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2PortCreateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2InitializeEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2Terminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2PortRegister(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2PortUnregister(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetAmbisonicsEncodeCoefficients(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetHrtfIdForCronos(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAutoMounterClient/Export.cpp b/core/libs/prx/libSceAutoMounterClient/Export.cpp index e55d28bc6a..3b3e49adb8 100644 --- a/core/libs/prx/libSceAutoMounterClient/Export.cpp +++ b/core/libs/prx/libSceAutoMounterClient/Export.cpp @@ -18,4 +18,19 @@ int APS5_VABI sceAutoMounterClientInit() { return 0; } +int APS5_VABI sceAutoMounterClientRegisterCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAutoMounterClientTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAutoMounterClientUnregisterCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceBluetoothHid/Export.cpp b/core/libs/prx/libSceBluetoothHid/Export.cpp index 46e728e4fd..98a466cddf 100644 --- a/core/libs/prx/libSceBluetoothHid/Export.cpp +++ b/core/libs/prx/libSceBluetoothHid/Export.cpp @@ -49,4 +49,49 @@ int APS5_VABI sceBluetoothHidSetOutputReport() { return 0; } +int APS5_VABI sceBluetoothHidDebugGetVersion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidDisconnectDevice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidGetDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidGetFeatureReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidGetReportDescriptor() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidInterruptOutput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidSetFeatureReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidThreadParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceCesCs/CMakeLists.txt b/core/libs/prx/libSceCesCs/CMakeLists.txt index 674db3453e..2601651d05 100644 --- a/core/libs/prx/libSceCesCs/CMakeLists.txt +++ b/core/libs/prx/libSceCesCs/CMakeLists.txt @@ -15,3 +15,20 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + foreach(variant IN ITEMS unicode unicode_module) + add_test_executable(guest_ces_${variant}_tests ${LIBS_INCLUDE_DIR}/tests/GuestCesUnicode.cpp) + target_include_directories(guest_ces_${variant}_tests PRIVATE ${LIBS_INCLUDE_DIR}) + if(variant STREQUAL "unicode_module") + target_link_libraries(guest_ces_${variant}_tests PRIVATE libSceCesCs-module) + else() + target_link_libraries(guest_ces_${variant}_tests PRIVATE libSceCesCs) + endif() + add_test(NAME guest_ces_${variant} COMMAND guest_ces_${variant}_tests) + endforeach() + if(WIN32) + set_tests_properties(guest_ces_unicode guest_ces_unicode_module PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceCesCs/CesLibrary.cmake b/core/libs/prx/libSceCesCs/CesLibrary.cmake index dcad1c7bd6..da7980798a 100644 --- a/core/libs/prx/libSceCesCs/CesLibrary.cmake +++ b/core/libs/prx/libSceCesCs/CesLibrary.cmake @@ -1,5 +1,7 @@ function(add_sce_ces_library target) - add_library(${target} SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Export.cpp) + add_library(${target} SHARED EXCLUDE_FROM_ALL + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Export.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Unicode.cpp) target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(${target}) endfunction() diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 0d21824d48..50005b98f8 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -494,15 +494,21 @@ int APS5_VABI sceCesUtf8ToSbc(const uint8_t* utf8, uint32_t utf8max, uint32_t* u return 0; } -int APS5_VABI sceCesEucJpToUtf8(void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucLength, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length) { - (void)profile; - (void)eucBuffer; - (void)eucMax; - (void)eucLength; - (void)utf8Buffer; - (void)utf8Max; - (void)utf8Length; - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceCesEucJpToUtf8(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucLength, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length) { + if (!isEucJp(profile)) return CES_ERROR_INVALID_PARAMETER; + if (!eucBuffer) return CES_ERROR_INVALID_SRC_BUFFER; + if (eucMax == 0) return CES_ERROR_SRC_BUFFER_END; + if (!utf8Buffer) return CES_ERROR_INVALID_DST_BUFFER; + uint32_t consumed = 0; + uint32_t code = 0; + if (const int result = decodeMbcs(profile, eucBuffer, eucMax, consumed, code); result != 0) return result; + uint8_t encoded[3]; + const uint32_t produced = encodeUtf8(code, encoded); + if (utf8Max < produced) return CES_ERROR_DST_BUFFER_END; + std::memcpy(utf8Buffer, encoded, produced); + if (eucLength) *eucLength = consumed; + if (utf8Length) *utf8Length = produced; + return 0; } int APS5_VABI sceCesEucKrToUtf8(void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucLength, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length) { diff --git a/core/libs/prx/libSceCesCs/Unicode.cpp b/core/libs/prx/libSceCesCs/Unicode.cpp new file mode 100644 index 0000000000..5a4b10399b --- /dev/null +++ b/core/libs/prx/libSceCesCs/Unicode.cpp @@ -0,0 +1,204 @@ +#include +#include +#include +#include "prx/libc/include/general/VabiMacros.hpp" + +namespace { + +constexpr int INVALID_SRC_BUFFER = static_cast(0x805C0010); +constexpr int SRC_BUFFER_END = static_cast(0x805C0011); +constexpr int INVALID_ENCODE = static_cast(0x805C0014); +constexpr int UNASSIGNED_CODE = static_cast(0x805C0020); +constexpr int INVALID_DST_BUFFER = static_cast(0x805C0030); +constexpr int DST_BUFFER_END = static_cast(0x805C0031); +constexpr bool NATIVE_BIG_ENDIAN = std::endian::native == std::endian::big; + +bool isScalar(uint32_t code) { + return code <= 0x10FFFF && (code < 0xD800 || code > 0xDFFF); +} + +int decodeUtf8(const uint8_t* source, uint32_t available, uint32_t& code, uint32_t& length) { + if (!source) return INVALID_SRC_BUFFER; + if (available == 0) return SRC_BUFFER_END; + const uint8_t lead = source[0]; + if (lead <= 0x7F) { code = lead; length = 1; return 0; } + if (lead >= 0xC2 && lead <= 0xDF) { code = lead & 0x1F; length = 2; } + else if (lead >= 0xE0 && lead <= 0xEF) { code = lead & 0x0F; length = 3; } + else if (lead >= 0xF0 && lead <= 0xF4) { code = lead & 0x07; length = 4; } + else return INVALID_ENCODE; + for (uint32_t i = 1; i < length; ++i) { + if (i >= available) return SRC_BUFFER_END; + const uint8_t next = source[i]; + if (next < 0x80 || next > 0xBF) return INVALID_ENCODE; + if (i == 1 && ((lead == 0xE0 && next < 0xA0) || (lead == 0xED && next > 0x9F) + || (lead == 0xF0 && next < 0x90) || (lead == 0xF4 && next > 0x8F))) return INVALID_ENCODE; + code = code << 6 | (next & 0x3F); + } + return 0; +} + +int encodeUtf8(uint32_t code, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + if (!isScalar(code)) return INVALID_ENCODE; + if (!target) return INVALID_DST_BUFFER; + uint8_t bytes[4]; + uint32_t length = 0; + if (code < 0x80) { bytes[0] = static_cast(code); length = 1; } + else if (code < 0x800) { + bytes[0] = static_cast(0xC0 | code >> 6); + bytes[1] = static_cast(0x80 | (code & 0x3F)); + length = 2; + } else if (code < 0x10000) { + bytes[0] = static_cast(0xE0 | code >> 12); + bytes[1] = static_cast(0x80 | (code >> 6 & 0x3F)); + bytes[2] = static_cast(0x80 | (code & 0x3F)); + length = 3; + } else { + bytes[0] = static_cast(0xF0 | code >> 18); + bytes[1] = static_cast(0x80 | (code >> 12 & 0x3F)); + bytes[2] = static_cast(0x80 | (code >> 6 & 0x3F)); + bytes[3] = static_cast(0x80 | (code & 0x3F)); + length = 4; + } + if (capacity < length) return DST_BUFFER_END; + std::memcpy(target, bytes, length); + if (targetLength) *targetLength = length; + return 0; +} + +template +uint32_t loadUnit(const Unit* source) { + const auto* bytes = reinterpret_cast(source); + uint32_t code = 0; + for (uint32_t i = 0; i < sizeof(Unit); ++i) code |= uint32_t(bytes[i]) << (8 * (BigEndian ? sizeof(Unit) - 1 - i : i)); + return code; +} + +template +void storeUnit(Unit* target, uint32_t code) { + auto* bytes = reinterpret_cast(target); + for (uint32_t i = 0; i < sizeof(Unit); ++i) bytes[i] = static_cast(code >> (8 * (BigEndian ? sizeof(Unit) - 1 - i : i))); +} + +template +int utf8ToUtf32(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint32_t* target) { + uint32_t code = 0; + uint32_t length = 0; + if (const int result = decodeUtf8(source, available, code, length); result != 0) return result; + if (!target) return INVALID_DST_BUFFER; + storeUnit(target, code); + if (sourceLength) *sourceLength = length; + return 0; +} + +template +int utf8ToUtf16(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint16_t* target, uint32_t capacity, uint32_t* targetLength) { + uint32_t code = 0; + uint32_t length = 0; + if (const int result = decodeUtf8(source, available, code, length); result != 0) return result; + if (!target) return INVALID_DST_BUFFER; + const uint32_t units = code < 0x10000 ? 1 : 2; + if (capacity < units) return DST_BUFFER_END; + if (units == 1) storeUnit(target, code); + else { + code -= 0x10000; + storeUnit(target, 0xD800 | code >> 10); + storeUnit(target + 1, 0xDC00 | (code & 0x3FF)); + } + if (sourceLength) *sourceLength = length; + if (targetLength) *targetLength = units; + return 0; +} + +template +int utf16ToUtf8(const uint16_t* source, uint32_t available, uint32_t* sourceLength, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + if (!source) return INVALID_SRC_BUFFER; + if (available == 0) return SRC_BUFFER_END; + uint32_t code = loadUnit(source); + uint32_t units = 1; + if (code >= 0xDC00 && code <= 0xDFFF) return INVALID_ENCODE; + if (code >= 0xD800 && code <= 0xDBFF) { + if (available < 2) return SRC_BUFFER_END; + const uint32_t low = loadUnit(source + 1); + if (low < 0xDC00 || low > 0xDFFF) return INVALID_ENCODE; + code = 0x10000 + ((code - 0xD800) << 10) + low - 0xDC00; + units = 2; + } + if (const int result = encodeUtf8(code, target, capacity, targetLength); result != 0) return result; + if (sourceLength) *sourceLength = units; + return 0; +} + +template +int utf32ToUtf8(const uint32_t* source, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + if (!source) return INVALID_SRC_BUFFER; + return encodeUtf8(loadUnit(source), target, capacity, targetLength); +} + +} + +extern "C" { + +int APS5_VABI sceCesUcs2ToUtf8(uint16_t source, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return encodeUtf8(source, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf8ToUcs2(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint16_t* target) { + uint32_t code = 0; + uint32_t length = 0; + if (const int result = decodeUtf8(source, available, code, length); result != 0) return result; + if (!target) return INVALID_DST_BUFFER; + if (code > 0xFFFF) return UNASSIGNED_CODE; + *target = static_cast(code); + if (sourceLength) *sourceLength = length; + return 0; +} + +int APS5_VABI sceCesUtf8ToUtf32(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint32_t* target) { + return utf8ToUtf32(source, available, sourceLength, target); +} + +int APS5_VABI sceCesUtf8ToUtf32be(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint32_t* target) { + return utf8ToUtf32(source, available, sourceLength, target); +} + +int APS5_VABI sceCesUtf8ToUtf32le(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint32_t* target) { + return utf8ToUtf32(source, available, sourceLength, target); +} + +int APS5_VABI sceCesUtf8ToUtf16(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint16_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf8ToUtf16(source, available, sourceLength, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf8ToUtf16be(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint16_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf8ToUtf16(source, available, sourceLength, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf8ToUtf16le(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint16_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf8ToUtf16(source, available, sourceLength, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf16ToUtf8(const uint16_t* source, uint32_t available, uint32_t* sourceLength, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf16ToUtf8(source, available, sourceLength, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf16beToUtf8(const uint16_t* source, uint32_t available, uint32_t* sourceLength, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf16ToUtf8(source, available, sourceLength, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf16leToUtf8(const uint16_t* source, uint32_t available, uint32_t* sourceLength, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf16ToUtf8(source, available, sourceLength, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf32ToUtf8(uint32_t source, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return encodeUtf8(source, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf32beToUtf8(const uint32_t* source, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf32ToUtf8(source, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf32leToUtf8(const uint32_t* source, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf32ToUtf8(source, target, capacity, targetLength); +} + +} diff --git a/core/libs/prx/libSceCompanionHttpd/Export.cpp b/core/libs/prx/libSceCompanionHttpd/Export.cpp index fb34206c21..48240a3fe1 100644 --- a/core/libs/prx/libSceCompanionHttpd/Export.cpp +++ b/core/libs/prx/libSceCompanionHttpd/Export.cpp @@ -5,6 +5,89 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceCompanionHttpdAddHeader() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdGet2ndScreenStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdGetEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdGetUserId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdInitialize2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdRegisterRequestBodyReceptionCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdRegisterRequestCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdRegisterRequestCallback2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdSetBody() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdSetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdStop() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdUnregisterRequestBodyReceptionCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdUnregisterRequestCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceCompanionUtil/Export.cpp b/core/libs/prx/libSceCompanionUtil/Export.cpp index fb34206c21..791221033d 100644 --- a/core/libs/prx/libSceCompanionUtil/Export.cpp +++ b/core/libs/prx/libSceCompanionUtil/Export.cpp @@ -5,6 +5,29 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceCompanionUtilGetEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionUtilGetRemoteOskEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionUtilInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionUtilOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionUtilTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceContentExport/Export.cpp b/core/libs/prx/libSceContentExport/Export.cpp index f618daeea1..b8a366cc65 100644 --- a/core/libs/prx/libSceContentExport/Export.cpp +++ b/core/libs/prx/libSceContentExport/Export.cpp @@ -55,6 +55,66 @@ int APS5_VABI sceContentExportFromFileWithThumbnail(void) { return 0; } +int APS5_VABI sceContentExportAddToFavorites(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportDataProvideFunction(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFree(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFrom(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFromFileForSf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFromFileWithContentIdList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFromFileWithTitleIdList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportMalloc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportRemoveFromFavorites(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportRenameContent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportValidateContents(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceContentExportStart(void) { std::lock_guard lock(g_sessionMutex); if (!g_initialized.load()) throw std::logic_error(std::string(__func__) + ": not initialized"); diff --git a/core/libs/prx/libSceContentSearch/Export.cpp b/core/libs/prx/libSceContentSearch/Export.cpp index 9475f46b16..a0a7553444 100644 --- a/core/libs/prx/libSceContentSearch/Export.cpp +++ b/core/libs/prx/libSceContentSearch/Export.cpp @@ -141,4 +141,53 @@ int APS5_VABI sceContentSearchTerm(void) { return 0; } +int APS5_VABI sceContentBinderEnd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentBinderGetNumOfDifference() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentBinderInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentBinderIsUpdated() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentBinderReset() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentBinderSearchDifference() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentBinderStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentBinderTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentSearchGetApplicationLastUpdateId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentSearchGetMyApplicationIndex() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentSearchGetNumOfApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentSearchSearchApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceCustomMusicCore/Export.cpp b/core/libs/prx/libSceCustomMusicCore/Export.cpp index fb34206c21..fa0c494e42 100644 --- a/core/libs/prx/libSceCustomMusicCore/Export.cpp +++ b/core/libs/prx/libSceCustomMusicCore/Export.cpp @@ -5,6 +5,94 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceCustomMusicCoreBgmClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreBgmOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreBgmOpen2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreBgmOutput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreBgmSetAudioVolume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreBgmStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreBgmStop() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreGetBgmAuthorityStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreGetHdcpStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreGetSystemAudioVolume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCorePostEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreSendEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreSendMulticastEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreSendSpMessage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreSetSystemAudioVolume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreStartOperation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreStartOperation2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreStopOperation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceDiscId/Export.cpp b/core/libs/prx/libSceDiscId/Export.cpp index fb34206c21..055e970b29 100644 --- a/core/libs/prx/libSceDiscId/Export.cpp +++ b/core/libs/prx/libSceDiscId/Export.cpp @@ -5,6 +5,14 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceDiscIdGet() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDiscIdGet2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceDiscMap/Export.cpp b/core/libs/prx/libSceDiscMap/Export.cpp index e1f2638a83..80f2a899dc 100644 --- a/core/libs/prx/libSceDiscMap/Export.cpp +++ b/core/libs/prx/libSceDiscMap/Export.cpp @@ -29,4 +29,9 @@ std::int32_t APS5_VABI sceDiscMapUnknown_ioKMruft1ek(const char* path, std::int6 return SCE_DISC_MAP_ERROR_NO_BITMAP_INFO; } +int APS5_VABI sceDiscMapGetPackageSize(std::int64_t fflags, int* ret1, int* ret2) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceErrorDialog/Export.cpp b/core/libs/prx/libSceErrorDialog/Export.cpp index 17d76ba8e4..4f17a4e50d 100644 --- a/core/libs/prx/libSceErrorDialog/Export.cpp +++ b/core/libs/prx/libSceErrorDialog/Export.cpp @@ -92,4 +92,14 @@ int APS5_VABI sceErrorDialogTerminate(void) { return kErrOk; } +int APS5_VABI sceErrorDialogOpenDetail() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceErrorDialogOpenWithReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceFiber/Export.cpp b/core/libs/prx/libSceFiber/Export.cpp index 021ab5269c..81aec33202 100644 --- a/core/libs/prx/libSceFiber/Export.cpp +++ b/core/libs/prx/libSceFiber/Export.cpp @@ -565,5 +565,24 @@ int32_t APS5_VABI sceFiberStopContextSizeCheck(void) { return g_contextSizeCheck.compare_exchange_strong(expected, false) ? SCE_OK : SCE_FIBER_ERROR_STATE; } +int APS5_VABI _sceFiberAttachContextAndRun(FiberObject* fiber, void* addr_context, uint64_t size_context, + uint64_t arg_on_run, uint64_t* arg_on_return) { + NotImplemented_nid_no_patch(__func__); + return 0; } +int APS5_VABI _sceFiberAttachContextAndSwitch(FiberObject* fiber, void* addr_context, uint64_t size_context, + uint64_t arg_on_run, uint64_t* arg_on_run_out) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceFiberInitializeWithInternalOptionImpl(FiberObject* fiber, const char* name, GuestFiberEntry entry, + uint64_t arg_on_initialize, void* addr_context, + uint64_t size_context, const FiberOptParam* opt_param, + uint32_t flags, uint32_t build_version) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceGameUpdate/Export.cpp b/core/libs/prx/libSceGameUpdate/Export.cpp index 7acb4eecc2..37c0901309 100644 --- a/core/libs/prx/libSceGameUpdate/Export.cpp +++ b/core/libs/prx/libSceGameUpdate/Export.cpp @@ -7,45 +7,79 @@ // Update checks need the network, which is not available; requests fail so the game skips the check. static constexpr int SCE_GAME_UPDATE_ERROR_OFFLINE = static_cast(0x80D50001); static constexpr int SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED = static_cast(0x80412801); +static constexpr int SCE_GAME_UPDATE_ERROR_ALREADY_INITIALIZED = static_cast(0x80412802); static constexpr int SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT = 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_assert(sizeof(GameUpdateCheckParam) == 0x30); +static_assert(sizeof(GameUpdateCheckResult) == 0x30); static std::atomic g_initialized{false}; +static int CheckRequestId(int request_id) { + if (request_id <= 0) { + return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; + } + if (!g_initialized.load()) { + return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; + } + return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; +} + +static bool HasNonZeroReserved(const GameUpdateCheckParam* param) { + for (std::uint32_t word : param->reserved) { + if (word != 0) { + return true; + } + } + return false; +} + extern "C" { int APS5_VABI sceGameUpdateAbortRequest(int request_id) { - (void)request_id; - return 0; + return CheckRequestId(request_id); } int APS5_VABI sceGameUpdateCheck(int request_id, const GameUpdateCheckParam* param, GameUpdateCheckResult* result) { - (void)request_id; - (void)param; - (void)result; - return SCE_GAME_UPDATE_ERROR_OFFLINE; + if (request_id <= 0) { + return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; + } + if (param == nullptr || result == nullptr) { + return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; + } + if (param->size != sizeof(GameUpdateCheckParam) || result->size != sizeof(GameUpdateCheckResult)) { + return SCE_GAME_UPDATE_ERROR_INVALID_SIZE; + } + if (param->option != 0 || HasNonZeroReserved(param)) { + return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; + } + return CheckRequestId(request_id); } int APS5_VABI sceGameUpdateCreateRequest(void) { + if (!g_initialized.load()) { + return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; + } return SCE_GAME_UPDATE_ERROR_OFFLINE; } int APS5_VABI sceGameUpdateDeleteRequest(int request_id) { - (void)request_id; - return 0; + return CheckRequestId(request_id); } int APS5_VABI sceGameUpdateGetAddcontLatestVersion(uint32_t service_label, const void* entitlement_label, GameUpdateAddcontVersionInfo* info) { (void)service_label; - if (!g_initialized.load()) { - return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; - } if (info == nullptr) { return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; } if (entitlement_label == nullptr) { return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; } + if (!g_initialized.load()) { + return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; + } if (info->size < sizeof(GameUpdateAddcontVersionInfo)) { return SCE_GAME_UPDATE_ERROR_INVALID_SIZE; } @@ -57,12 +91,16 @@ int APS5_VABI sceGameUpdateGetAddcontLatestVersion(uint32_t service_label, const } int APS5_VABI sceGameUpdateInitialize(void) { - g_initialized.store(true); + if (g_initialized.exchange(true)) { + return SCE_GAME_UPDATE_ERROR_ALREADY_INITIALIZED; + } return 0; } int APS5_VABI sceGameUpdateTerminate(void) { - g_initialized.store(false); + if (!g_initialized.exchange(false)) { + return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; + } return 0; } diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index 36c2ca3c06..0112025f52 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -1,4 +1,5 @@ #include "Hmd.hpp" +#include "prx/libc/include/General.hpp" #include @@ -68,4 +69,799 @@ std::int32_t APS5_VABI sceHmdInternalMmapIsConnect() { return 0; } +int APS5_VABI sceHmdInternalGetPUVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalEventInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetVirtualDisplaySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSocialScreenSetOutput(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetVirtualDisplayDepth(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetEyeStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionUnsetCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetCrashReportCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDebugTextMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetWorkMemoryAlign(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalRegisterEventCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetWorkMemorySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartWideNear(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetPUSerialNumber(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDebugSocialScreenMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetM2LedBrightness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetIPD(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMirroringModeSetAspect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenSendVideo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetRequiredPUPVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionSetCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuCheckAfterPvt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalResetInertialSensor(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionSetUserEventStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionSetOutputMinColor(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetVirtualDisplayDepth(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetDistortionWorkMemoryAlign(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartMultilayer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalPushVr2dData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionClearUserEventEnd(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetIPD(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetVRMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCrashReportReadDataSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetVr2dData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenSendAudio(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetPuPositionParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionInitializeCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGet2DEyeOffset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDebugSocialScreenMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetHmuPowerStatusForDebug(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetDistortionWorkMemorySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionSetDisplayBuffers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenSetFadeAndSwitch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCreateSharedMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMirroringModeSetAspectDebug(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetPuBuildNumber(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetCompoundEyeCorrectionCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetS3dPassMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStopCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalIsCommonDlgMiniAppVr2d(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMmapGetSensorCalibrationData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetCorrectionCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionQueryOnionBuffAlign(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDebugMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalIsMiniAppVr2d(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalIsCommonDlgVr2d(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionSetOutputMinColor(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetPortConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionDebugGetLastInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetM2LedOn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetFieldOfView(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionAddDisplayBuffer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetIpdSettingEnableForSystemService(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetLedOn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternal3dAudioSendData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDeviceInformationByHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStopLiveCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCrashReportReadData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDeviceInformation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDefaultLedData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionQueryGarlicBuffAlign(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetHmuPowerControl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalBindDeviceWithUserId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetHmuSerialNumber(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetWideNearCorrectionCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCheckS3dPassModeAvailable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenGetAudioStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeGetAudioStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartLiveCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalIsGameVr2d(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetAssyError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionFinalize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSocialScreenSetFadeAndSwitch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetVirtualDisplayHeight(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuSetMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetForcedCrash(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuCheckPartialUpdateAvailable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDebugTextMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCrashReportClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetHmuPowerControlForDebug(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetVirtualDisplayHeight(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalResetLedForVsh(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDebugMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCrashReportCancel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeGetVideoStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCheckDeviceModelMk3(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdSetVr2dEnable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetSidetone(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMmapGetModeId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGet2dVrCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuSendSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetWideNearDistortionCorrectionCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartMultilayer2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetDistortionCorrectionCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDeviceConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetVirtualDisplaySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionUnsetDisplayBuffers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetHmuOpticalParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeSendVideo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalResetLedForVrTracker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionQueryOnionBuffSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartWithOverlay(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionSetUserEventEnd(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCrashReportOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDemoMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartWideNearWithOverlay(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetDistortionParams(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuSend(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionClearUserEventStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetUserType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDemoMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMapSharedMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetCrashDumpInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetTv4kCapability(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuReset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStart2dVr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMmapGetCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDefaultLedData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetInertialSensorData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternal3dAudioClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetBrightness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSocialScreenGetFadeState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeSendAudio(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetStatusReport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetBrightness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionFinalizeCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetPortStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenGetVideoStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternal3dAudioOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenGetFadeState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDebugGpo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetIpdSettingEnableForSystemService(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionDebugGetLastInfoMultilayer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetPuRevision(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionQueryGarlicBuffSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGet2dVrCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index f2ade7c5ed..760deee096 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -48,9 +48,8 @@ int APS5_VABI sceHmd2ReprojectionGetStatus() { return 0; } -int APS5_VABI sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign() { - NotImplemented_nid_no_patch(__func__); - return 0; +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign() { + return {0x1106000, 0x100}; } std::int32_t APS5_VABI sceHmd2Initialize(const void* param) { @@ -149,14 +148,12 @@ int APS5_VABI sceHmd2ReprojectionEnableVrMode() { return 0; } -int APS5_VABI sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign() { - NotImplemented_nid_no_patch(__func__); - return 0; +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign() { + return {0x5FA0000, 0x10000}; } -int APS5_VABI sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign() { - NotImplemented_nid_no_patch(__func__); - return 0; +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign() { + return {0x4000, 0x100}; } int APS5_VABI sceHmd2ReprojectionGetPredictedDisplayTime() { @@ -174,9 +171,8 @@ int APS5_VABI sceHmd2ReprojectionQueryBufferSizeAlign() { return 0; } -int APS5_VABI sceHmd2ReprojectionQueryDisplayBufferSizeAlign() { - NotImplemented_nid_no_patch(__func__); - return 0; +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQueryDisplayBufferSizeAlign() { + return {0x6000000, 0x10000}; } int APS5_VABI sceHmd2ReprojectionSetAllowPositionalReprojection() { diff --git a/core/libs/prx/libSceHmd2/Hmd2.hpp b/core/libs/prx/libSceHmd2/Hmd2.hpp index 3594b32bd1..5e978e686f 100644 --- a/core/libs/prx/libSceHmd2/Hmd2.hpp +++ b/core/libs/prx/libSceHmd2/Hmd2.hpp @@ -49,6 +49,13 @@ static_assert(offsetof(DeviceInformation, hmuMount) == 20); static_assert(offsetof(DeviceInformation, virtualImageDistance) == 24); static_assert(sizeof(FieldOfView) == 16); +struct SizeAlign { + std::uint64_t size; + std::uint64_t align; +}; + +static_assert(sizeof(SizeAlign) == 16); + } extern "C" { @@ -61,6 +68,10 @@ std::int32_t APS5_VABI sceHmd2GetDeviceInformationByHandle(std::int32_t handle, std::int32_t APS5_VABI sceHmd2GetFieldOfView(std::int32_t handle, Hmd2::FieldOfView* fieldOfView); std::int32_t APS5_VABI sceHmd2GetFieldOfViewWithoutHandle(Hmd2::FieldOfView* fieldOfView); std::int32_t APS5_VABI sceHmd2SetVibration(std::int32_t handle, std::uint32_t level); +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQueryDisplayBufferSizeAlign(); +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign(); +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign(); +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign(); } #endif diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index a1de4ee746..04e5b51e72 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -609,4 +609,250 @@ int APS5_VABI sceHttpsFreeCaList(int http_ctx_id, HttpsCaList* ca_list) { ca_list->certsNum = 0; return 0; } + +int APS5_VABI sceHttpAuthCacheImport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpAuthCacheExport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgEnableProfile(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgSetPrintf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgShowStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetMemoryPoolStats(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgShowConnectionStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgShowRequestStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgShowMemoryPoolStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgGetConnectionStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgGetRequestStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpAbortRequestForce(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpAddRequestHeaderRaw(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpRemoveRequestHeader(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetAuthEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetAutoRedirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpTrySetNonblock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetNonblock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpTryGetNonblock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetEpollId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetEpollId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetEpoll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetCookie(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetCookieEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpAddCookie(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieSendCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCookieExport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCookieImport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieTotalMaxSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieMaxSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieMaxNum(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieMaxNumPerDomain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetCookieStats(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpsEnableOptionPrivate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpsDisableOptionPrivate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetPriorityOption(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetPolicyOption(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpRequestGetAllHeaders(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetAcceptEncodingGZIPEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetAcceptEncodingGZIPEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetDefaultAcceptEncodingGZIPEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetProxy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetSocketCreationCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetHttp09Enabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetRegisteredCtxIds(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetConnectionStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpAddQuery(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetDelayBuildRequestEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRedirectedConnectionEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp index 2268da4681..ac9424a75a 100644 --- a/core/libs/prx/libSceHttp/src/Uri.cpp +++ b/core/libs/prx/libSceHttp/src/Uri.cpp @@ -1,5 +1,4 @@ #include -#include #include #include #include @@ -34,10 +33,22 @@ struct UriParts { uint16_t port = 0; }; +bool isAsciiAlpha(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); +} + +bool isAsciiDigit(char c) { + return c >= '0' && c <= '9'; +} + +char toAsciiLower(char c) { + return c >= 'A' && c <= 'Z' ? static_cast(c + ('a' - 'A')) : c; +} + bool equalsIgnoreCase(std::string_view left, std::string_view right) { if (left.size() != right.size()) return false; for (size_t i = 0; i < left.size(); ++i) { - if (std::tolower(static_cast(left[i])) != std::tolower(static_cast(right[i]))) return false; + if (toAsciiLower(left[i]) != toAsciiLower(right[i])) return false; } return true; } @@ -49,7 +60,7 @@ uint16_t defaultPort(std::string_view scheme) { } bool isHostCharacter(char c) { - return std::isalnum(static_cast(c)) || c == '-' || c == '.' || c == '_'; + return isAsciiAlpha(c) || isAsciiDigit(c) || c == '-' || c == '.' || c == '_'; } std::string removeDotSegments(std::string_view input) { @@ -76,9 +87,9 @@ std::string removeDotSegments(std::string_view input) { size_t schemeLength(std::string_view uri) { size_t schemeEnd = 0; - if (!uri.empty() && std::isalpha(static_cast(uri[0]))) { + if (!uri.empty() && isAsciiAlpha(uri[0])) { schemeEnd = 1; - while (schemeEnd < uri.size() && (std::isalnum(static_cast(uri[schemeEnd])) || uri[schemeEnd] == '+' + while (schemeEnd < uri.size() && (isAsciiAlpha(uri[schemeEnd]) || isAsciiDigit(uri[schemeEnd]) || uri[schemeEnd] == '+' || uri[schemeEnd] == '-' || uri[schemeEnd] == '.')) ++schemeEnd; } return schemeEnd > 0 && schemeEnd < uri.size() && uri[schemeEnd] == ':' ? schemeEnd : 0; @@ -133,7 +144,7 @@ int parseUri(std::string_view uri, UriParts& parts) { if (port.size() > 5) return ERROR_INVALID_URL; uint32_t portValue = 0; for (const char c : port) { - if (!std::isdigit(static_cast(c))) return ERROR_INVALID_URL; + if (!isAsciiDigit(c)) return ERROR_INVALID_URL; portValue = portValue * 10 + static_cast(c - '0'); } if (portValue > 0xFFFF) return ERROR_INVALID_URL; diff --git a/core/libs/prx/libSceHttpCache/Export.cpp b/core/libs/prx/libSceHttpCache/Export.cpp index fb34206c21..f76492fc41 100644 --- a/core/libs/prx/libSceHttpCache/Export.cpp +++ b/core/libs/prx/libSceHttpCache/Export.cpp @@ -5,6 +5,114 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceHttpCacheClear() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheClearAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheCompleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheCreateRequestWithTag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheReadData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRetrieve() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRetrieveWithMemoryPool() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRevalidate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRevalidateWithMemoryPool() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSetCacheSharing() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSetData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSetQuota() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSetResponseHeader() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSystemClearAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSystemInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSystemSendStatistics() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSystemShutdown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSystemTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 575463cf73..a2979dea88 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceIme/include/ImeInput.hpp" @@ -59,9 +60,17 @@ constexpr uint32_t CaretMoveRight = 2; struct Session { Param param{}; + ImeExtKeyboardFilter extKeyboardFilter = nullptr; std::u16string text; std::uint32_t caret = 0; std::deque events; + std::deque hostInputs; + std::uint64_t id = 0; + std::uint64_t revision = 0; +}; + +struct DeferredFilter { + std::u16string oldText; }; std::mutex g_keyboardMutex; @@ -69,6 +78,7 @@ std::set g_openKeyboards; std::mutex g_imeMutex; std::optional g_session; std::atomic g_textSink{nullptr}; +std::uint64_t g_nextSessionId = 0; void PanelSize(const Param& param, uint32_t* width, uint32_t* height) { const uint32_t scale = (param.option & OptionUseOver2K) != 0 ? 2 : 1; @@ -157,7 +167,68 @@ void QueueCaretSteps(Session& session, uint32_t direction, std::size_t steps) { } } -void Insert(Session& session, std::u16string units) { +struct EditDelta { + std::uint32_t index = 0; + std::int32_t length = 0; +}; + +EditDelta ComputeEditDelta(const std::u16string& before, const std::u16string& after) { + std::size_t prefix = 0; + while (prefix < before.size() && prefix < after.size() && before[prefix] == after[prefix]) ++prefix; + std::size_t beforeTail = before.size(); + std::size_t afterTail = after.size(); + while (beforeTail > prefix && afterTail > prefix && before[beforeTail - 1] == after[afterTail - 1]) { + --beforeTail; + --afterTail; + } + return {static_cast(prefix), + static_cast(afterTail - prefix) - static_cast(beforeTail - prefix)}; +} + +void NormalizeCaret(Session& session) { + if (session.caret > session.text.size()) session.caret = static_cast(session.text.size()); + if (session.caret > 0 && session.caret < session.text.size() && + IsHighSurrogate(session.text[session.caret - 1]) && IsLowSurrogate(session.text[session.caret])) { + ++session.caret; + } +} + +bool RunTextFilter(ImeTextFilter filter, const std::u16string& text, std::uint32_t maxLength, bool multiline, + std::u16string* filtered) { + std::vector buffer(maxLength + 1, u'\0'); + std::uint32_t length = maxLength; + if (filter(buffer.data(), &length, text.c_str(), static_cast(text.size())) != 0) { + return false; + } + if (length > maxLength || !ValidText(buffer.data(), length, multiline)) { + return false; + } + filtered->assign(buffer.data(), buffer.data() + length); + return true; +} + +void CommitFilteredEdit(Session& session, const std::u16string* filtered, const std::u16string& oldText) { + if (filtered) { + session.text = *filtered; + NormalizeCaret(session); + } + if (session.text == oldText) return; + SyncBuffers(session); + const EditDelta edit = ComputeEditDelta(oldText, session.text); + QueueText(session, EventUpdateText, edit.index, edit.length); +} + +std::optional CommitEdit(Session& session, const std::u16string& oldText, std::uint32_t plainIndex, + std::int32_t plainDelta) { + if (session.param.filter) { + return DeferredFilter{oldText}; + } + SyncBuffers(session); + QueueText(session, EventUpdateText, plainIndex, plainDelta); + return std::nullopt; +} + +std::optional Insert(Session& session, std::u16string units) { const bool multiline = (session.param.option & OptionMultiline) != 0; std::u16string accepted; for (const char16_t unit : units) { @@ -167,12 +238,12 @@ void Insert(Session& session, std::u16string units) { const std::size_t room = session.param.max_text_length - session.text.size(); std::size_t count = std::min(accepted.size(), room); if (count > 0 && count < accepted.size() && IsHighSurrogate(accepted[count - 1])) --count; - if (count == 0) return; + if (count == 0) return std::nullopt; const uint32_t index = session.caret; + const std::u16string oldText = session.text; session.text.insert(index, accepted, 0, count); session.caret += static_cast(count); - SyncBuffers(session); - QueueText(session, EventUpdateText, index, static_cast(count)); + return CommitEdit(session, oldText, index, static_cast(count)); } std::size_t UnitsBefore(const Session& session) { @@ -187,65 +258,61 @@ std::size_t UnitsAfter(const Session& session) { return caret + 1 < session.text.size() && IsHighSurrogate(session.text[caret]) && IsLowSurrogate(session.text[caret + 1]) ? 2 : 1; } -void Apply(Session& session, const ImeHostInput& input) { +std::optional Apply(Session& session, const ImeHostInput& input) { switch (input.kind) { case ImeHostInputKind::Text: { std::size_t size = 0; while (size < sizeof(input.text) && input.text[size] != '\0') ++size; - Insert(session, DecodeUtf8(input.text, size)); - return; + return Insert(session, DecodeUtf8(input.text, size)); } case ImeHostInputKind::Backspace: { const std::size_t units = UnitsBefore(session); - if (units == 0) return; + if (units == 0) return std::nullopt; + const std::u16string oldText = session.text; session.caret -= static_cast(units); session.text.erase(session.caret, units); - SyncBuffers(session); - QueueText(session, EventUpdateText, session.caret, -static_cast(units)); - return; + return CommitEdit(session, oldText, session.caret, -static_cast(units)); } case ImeHostInputKind::Delete: { const std::size_t units = UnitsAfter(session); - if (units == 0) return; + if (units == 0) return std::nullopt; + const std::u16string oldText = session.text; session.text.erase(session.caret, units); - SyncBuffers(session); - QueueText(session, EventUpdateText, session.caret, -static_cast(units)); - return; + return CommitEdit(session, oldText, session.caret, -static_cast(units)); } case ImeHostInputKind::Left: { const std::size_t units = UnitsBefore(session); session.caret -= static_cast(units); QueueCaretSteps(session, CaretMoveLeft, units); - return; + return std::nullopt; } case ImeHostInputKind::Right: { const std::size_t units = UnitsAfter(session); session.caret += static_cast(units); QueueCaretSteps(session, CaretMoveRight, units); - return; + return std::nullopt; } case ImeHostInputKind::Home: { const std::size_t units = session.caret; session.caret = 0; QueueCaretSteps(session, CaretMoveLeft, units); - return; + return std::nullopt; } case ImeHostInputKind::End: { const std::size_t units = session.text.size() - session.caret; session.caret = static_cast(session.text.size()); QueueCaretSteps(session, CaretMoveRight, units); - return; + return std::nullopt; } case ImeHostInputKind::Enter: if ((session.param.option & OptionMultiline) != 0) { - Insert(session, u"\n"); - } else { - QueueText(session, EventPressEnter, session.caret, 0); + return Insert(session, u"\n"); } - return; + QueueText(session, EventPressEnter, session.caret, 0); + return std::nullopt; case ImeHostInputKind::Escape: QueueText(session, EventPressClose, session.caret, 0); - return; + return std::nullopt; } throw std::invalid_argument("IME host input: unknown input kind"); } @@ -289,7 +356,7 @@ void ImePublishInput_nid_postfix(const ImeHostInput& input) { { std::lock_guard lock(g_imeMutex); if (g_session) { - Apply(*g_session, input); + g_session->hostInputs.push_back(input); return; } } @@ -317,10 +384,10 @@ int APS5_VABI sceImeOpen_nid_postfix(const Param* param, const ExtendedParam* ex std::lock_guard lock(g_imeMutex); if (g_session) return ErrorBusy; if (const int error = ValidateOpen(param, extended)) return error; - if (param->filter) NotImplemented_nid_no_patch("sceImeOpen: text filter"); - if (extended && extended->ext_keyboard_filter) NotImplemented_nid_no_patch("sceImeOpen: external keyboard filter"); Session session; + session.id = ++g_nextSessionId; session.param = *param; + session.extKeyboardFilter = extended ? extended->ext_keyboard_filter : nullptr; session.text.assign(param->input_text_buffer, TextLength(param->input_text_buffer, param->max_text_length)); session.caret = static_cast(session.text.size()); SyncBuffers(session); @@ -412,6 +479,13 @@ int APS5_VABI sceImeSetCaret(const Caret* caret) { if (!caret) return ErrorInvalidAddress; if (caret->index > g_session->text.size()) return ErrorInvalidParam; g_session->caret = caret->index; + ++g_session->revision; + return 0; +} + +int APS5_VABI sceImeSetConsecutiveClauseConversionEnabled(bool enabled) { + (void)enabled; + NotImplemented_nid_no_patch(__func__); return 0; } @@ -423,6 +497,7 @@ int APS5_VABI sceImeSetText(const char16_t* text, uint32_t length) { const std::size_t count = TextLength(text, std::min(length, session.param.max_text_length)); if (!ValidText(text, count, (session.param.option & OptionMultiline) != 0)) return ErrorInvalidText; session.text.assign(text, count); + ++session.revision; if (session.caret > session.text.size()) session.caret = static_cast(session.text.size()); SyncBuffers(session); return 0; @@ -441,15 +516,272 @@ int APS5_VABI sceImeUpdate(EventHandler handler) { for (;;) { ImeEvent event{}; void* arg = nullptr; + bool deliver = false; + bool runFilter = false; + ImeTextFilter filter = nullptr; + std::u16string filterText; + std::u16string filterOldText; + std::uint32_t filterMaxLength = 0; + bool filterMultiline = false; + std::uint64_t sessionId = 0; + std::uint64_t sessionRevision = 0; { std::lock_guard lock(g_imeMutex); - if (!g_session || g_session->param.handler != handler || g_session->events.empty()) return 0; - event = g_session->events.front(); - g_session->events.pop_front(); - arg = g_session->param.arg; + if (!g_session || g_session->param.handler != handler) return 0; + if (!g_session->hostInputs.empty()) { + const ImeHostInput input = g_session->hostInputs.front(); + g_session->hostInputs.pop_front(); + if (const std::optional deferred = Apply(*g_session, input)) { + runFilter = true; + filter = g_session->param.filter; + filterText = g_session->text; + filterOldText = deferred->oldText; + filterMaxLength = g_session->param.max_text_length; + filterMultiline = (g_session->param.option & OptionMultiline) != 0; + sessionId = g_session->id; + sessionRevision = g_session->revision; + } + } else if (!g_session->events.empty()) { + event = g_session->events.front(); + g_session->events.pop_front(); + arg = g_session->param.arg; + deliver = true; + } else { + return 0; + } } + if (runFilter) { + std::u16string filtered; + const bool accepted = RunTextFilter(filter, filterText, filterMaxLength, filterMultiline, &filtered); + std::lock_guard lock(g_imeMutex); + if (g_session && g_session->id == sessionId) { + if (g_session->revision == sessionRevision) { + CommitFilteredEdit(*g_session, accepted ? &filtered : nullptr, filterOldText); + } else if (g_session->text == filterText) { + g_session->text = filterOldText; + NormalizeCaret(*g_session); + } + } + continue; + } + if (!deliver) continue; handler(arg, &event); } } +int APS5_VABI sceImeVshOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeKeyboardUpdate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshUpdate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshUpdateContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshClearPreedit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifySetText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetCaretGeometry(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshConfirmPreedit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetCaretIndexInPreedit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetPreeditGeometry(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetSelectGeometry(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshGetPanelPositionAndForm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeSetCandidateIndex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeConfirmCandidate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDisableController(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeSetVoiceDictationEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeSetEditEventWithRemotePlayEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeForTestFunction(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshInformConfirmdString2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetSelectionText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeCheckUpdateTextInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeCheckRemoteEventParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeKeyboardOpenInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshUpdateContext2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSendTextInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshInformConfirmdString(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyCurrentTextInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyCurrentTextInfoForReconvertText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyDeleteText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyUpdateCaretText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshCheckAutoReConvertingPreeditText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetPanelPosition(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyPreeditMetaInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeConfigSet(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeConfigGet(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicGetWords(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicAddWord(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicDeleteWord(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicReplaceWord(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicDeleteLearnDics(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicDeleteUserDics(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceImeDialog/Export.cpp b/core/libs/prx/libSceImeDialog/Export.cpp index 71df834abb..9eea28ac6d 100644 --- a/core/libs/prx/libSceImeDialog/Export.cpp +++ b/core/libs/prx/libSceImeDialog/Export.cpp @@ -175,4 +175,29 @@ int APS5_VABI sceImeDialogTerm(void) { return 0; } +int APS5_VABI sceImeDialogForTestFunction() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDialogForceClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDialogGetCurrentStarState(int64_t state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDialogInitInternal(Param* param, ExtendedParam* extended) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDialogSetPanelPosition(int32_t posx, int32_t posy) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceJpegDec/Export.cpp b/core/libs/prx/libSceJpegDec/Export.cpp index 0d92c485bb..1c67856235 100644 --- a/core/libs/prx/libSceJpegDec/Export.cpp +++ b/core/libs/prx/libSceJpegDec/Export.cpp @@ -9,6 +9,7 @@ #include #include "Decoder/Jpeg.hpp" #include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" static_assert(sizeof(JpegDecCreateParam) == 0xC); static_assert(sizeof(JpegDecParseParam) == 0x10); @@ -32,6 +33,9 @@ constexpr std::uintptr_t HANDLE_ALIGNMENT = 8; constexpr std::uint16_t DECODE_MODE_NORMAL = 0; constexpr std::uint16_t DOWN_SCALE_NONE = 1; +constexpr std::uint16_t DOWN_SCALE_HALF = 2; +constexpr std::uint16_t DOWN_SCALE_QUARTER = 4; +constexpr std::uint16_t DOWN_SCALE_EIGHTH = 8; constexpr std::uint16_t PIXEL_FORMAT_R8G8B8A8 = 0; constexpr std::uint16_t PIXEL_FORMAT_B8G8R8A8 = 1; @@ -55,9 +59,11 @@ std::int32_t validateCreateParam(const JpegDecCreateParam* param) { return 0; } -void requireNormalDecode(const char* function, std::uint16_t decodeMode, std::uint16_t downScale) { +std::uint32_t requireDownScale(const char* function, std::uint16_t decodeMode, std::uint16_t downScale) { if (decodeMode != DECODE_MODE_NORMAL) throw std::runtime_error(std::string(function) + ": decode mode " + std::to_string(decodeMode) + " is not implemented"); - if (downScale != DOWN_SCALE_NONE) throw std::runtime_error(std::string(function) + ": down scale " + std::to_string(downScale) + " is not implemented"); + if (downScale != DOWN_SCALE_NONE && downScale != DOWN_SCALE_HALF && downScale != DOWN_SCALE_QUARTER && downScale != DOWN_SCALE_EIGHTH) + throw std::runtime_error(std::string(function) + ": down scale " + std::to_string(downScale) + " is not implemented"); + return downScale; } Context* toContext(void* handle) { @@ -67,15 +73,15 @@ Context* toContext(void* handle) { return context->self == context ? context : nullptr; } -void fillImageInfo(const Decoder::Jpeg::Header& header, JpegDecImageInfo* info) { +void fillImageInfo(const Decoder::Jpeg::Header& header, std::uint32_t downScale, JpegDecImageInfo* info) { *info = {}; info->image_width = header.width; info->image_height = header.height; info->color_space = COLOR_SPACE_RGB; info->component_number = static_cast(header.channels); info->suitable_csc_attribute = ATTRIBUTE_CSC_ORDINARY; - info->output_image_width = header.width; - info->output_image_height = header.height; + info->output_image_width = Decoder::Jpeg::ScaledDimension(header.width, downScale); + info->output_image_height = Decoder::Jpeg::ScaledDimension(header.height, downScale); } std::span toBytes(const void* data, std::uint32_t size) { @@ -108,19 +114,21 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param if (!param->jpeg_mem_addr || !param->image_mem_addr) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (param->jpeg_mem_size == 0 || param->image_mem_size == 0) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; if (param->pixel_format != PIXEL_FORMAT_R8G8B8A8 && param->pixel_format != PIXEL_FORMAT_B8G8R8A8) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; - requireNormalDecode(__func__, param->decode_mode, param->down_scale); + const std::uint32_t downScale = requireDownScale(__func__, param->decode_mode, param->down_scale); const std::span jpeg = toBytes(param->jpeg_mem_addr, param->jpeg_mem_size); const std::optional header = Decoder::Jpeg::ParseHeader(jpeg); if (!header) return SCE_JPEG_DEC_ERROR_INVALID_DATA; + const std::uint32_t outputWidth = Decoder::Jpeg::ScaledDimension(header->width, downScale); + const std::uint32_t outputHeight = Decoder::Jpeg::ScaledDimension(header->height, downScale); - const std::uint64_t rowSize = static_cast(header->width) * BYTES_PER_PIXEL; + const std::uint64_t rowSize = static_cast(outputWidth) * BYTES_PER_PIXEL; const std::uint64_t pitch = param->image_pitch == 0 ? rowSize : param->image_pitch; if (pitch < rowSize) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; - if ((header->height - 1) * pitch + rowSize > param->image_mem_size) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + if ((outputHeight - 1) * pitch + rowSize > param->image_mem_size) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; - const std::optional image = Decoder::Jpeg::Decode(jpeg); - if (!image || image->width != header->width || image->height != header->height) return SCE_JPEG_DEC_ERROR_DECODE_ERROR; + const std::optional image = downScale == DOWN_SCALE_NONE ? Decoder::Jpeg::Decode(jpeg) : Decoder::Jpeg::DecodeScaled(jpeg, downScale); + if (!image || image->width != outputWidth || image->height != outputHeight) return SCE_JPEG_DEC_ERROR_DECODE_ERROR; const bool swapRedBlue = param->pixel_format == PIXEL_FORMAT_B8G8R8A8; const auto alpha = static_cast(param->alpha_value); @@ -144,9 +152,9 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param } } - if (image_info) fillImageInfo(*header, image_info); - if (header->width > MAX_PACKED_DIMENSION || header->height > MAX_PACKED_DIMENSION) return 0; - return static_cast(header->width << 16 | header->height); + if (image_info) fillImageInfo(*header, downScale, image_info); + if (outputWidth > MAX_PACKED_DIMENSION || outputHeight > MAX_PACKED_DIMENSION) return 0; + return static_cast(outputWidth << 16 | outputHeight); } int32_t APS5_VABI sceJpegDecDelete(void* handle) { @@ -160,10 +168,10 @@ int32_t APS5_VABI sceJpegDecParseHeader(const JpegDecParseParam* param, JpegDecI if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; if (!param->jpeg_mem_addr || !image_info) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (param->jpeg_mem_size == 0) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; - requireNormalDecode(__func__, param->decode_mode, param->down_scale); + const std::uint32_t downScale = requireDownScale(__func__, param->decode_mode, param->down_scale); const std::optional header = Decoder::Jpeg::ParseHeader(toBytes(param->jpeg_mem_addr, param->jpeg_mem_size)); if (!header) return SCE_JPEG_DEC_ERROR_INVALID_DATA; - fillImageInfo(*header, image_info); + fillImageInfo(*header, downScale, image_info); return 0; } @@ -173,4 +181,9 @@ int32_t APS5_VABI sceJpegDecQueryMemorySize(const JpegDecCreateParam* param) { return static_cast(MEMORY_SIZE); } +int APS5_VABI sceJpegDecDecodeWithInputControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 744ba1497a..6231181390 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -196,12 +196,15 @@ void CopyInto(Value& destination, const Value& source) { } void SetType(Node& n, ValueType type) { - Clear(n); - n.type = type; - if (type == TypeString) Construct(n.string, std::string()); - if (type == TypeArray) Construct(n.array); - if (type == TypeObject) Construct(n.object); if (type < TypeNull || type > TypeObject) throw std::invalid_argument("sce::Json::Value::set: invalid value type"); + Node pending{}; + pending.type = type; + if (type == TypeString) Construct(pending.string, std::string()); + if (type == TypeArray) Construct(pending.array); + if (type == TypeObject) Construct(pending.object); + Clear(n); + pending.handlers = n.handlers; + n = pending; } const Value& Defaults(ValueType type) { diff --git a/core/libs/prx/libSceKeyboard/Export.cpp b/core/libs/prx/libSceKeyboard/Export.cpp index d94e23040f..d8d408237a 100644 --- a/core/libs/prx/libSceKeyboard/Export.cpp +++ b/core/libs/prx/libSceKeyboard/Export.cpp @@ -98,6 +98,36 @@ int APS5_VABI sceKeyboardSetProcessPrivilege(void) { return 0; } +int APS5_VABI sceDbgKeyboardClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDbgKeyboardGetKey2Char() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDbgKeyboardInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDbgKeyboardOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDbgKeyboardRead() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDbgKeyboardReadState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + void KeyboardPublishInput_nid_postfix(const KeyboardInputEvent& event) { Keyboard::Publish(event); } diff --git a/core/libs/prx/libSceLibcInternal/Directory.cpp b/core/libs/prx/libSceLibcInternal/Directory.cpp index 937f52fd97..48a1614e85 100644 --- a/core/libs/prx/libSceLibcInternal/Directory.cpp +++ b/core/libs/prx/libSceLibcInternal/Directory.cpp @@ -1,16 +1,21 @@ #include "prx/libc/include/GuestDirectory.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include +#include extern "C" int APS5_VABI close_nid_postfix(int d); +extern "C" int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...); extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" void* APS5_VABI fdopendir_nid_postfix(int fd) { + constexpr int SetDescriptorFlags = 2; + constexpr int CloseOnExec = 1; + if (fcntl_nid_postfix(fd, SetDescriptorFlags, CloseOnExec) == -1) return nullptr; int error = 0; const char* path = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); if (path == nullptr) { *__error_nid_postfix() = error == EBADF ? 9 : 20; return nullptr; } - return OpenDescriptorDirectory_nid_no_patch(path, fd, close_nid_postfix); + return OpenDescriptorDirectory_nid_no_patch(path, fd, close_nid_postfix, GetDirectoryDescriptorPosition_nid_no_patch(fd)); } diff --git a/core/libs/prx/libSceLibcInternal/PathStatistics.cpp b/core/libs/prx/libSceLibcInternal/PathStatistics.cpp index 102ff774bb..c66f3dcc3b 100644 --- a/core/libs/prx/libSceLibcInternal/PathStatistics.cpp +++ b/core/libs/prx/libSceLibcInternal/PathStatistics.cpp @@ -140,7 +140,7 @@ int OpenMetadata(const std::filesystem::path& path, bool directory) { } extern "C" int APS5_VABI statvfs_nid_postfix(const char* path, LibcStatvfs* output) { - if (path == nullptr) return Fail(14); + if (path == nullptr || output == nullptr) return Fail(14); if (*path == '\0') return Fail(2); const int saved = errno; const auto last = std::string_view(path).back(); diff --git a/core/libs/prx/libSceLoginDialog/Export.cpp b/core/libs/prx/libSceLoginDialog/Export.cpp index 96058fbe6f..7ca5a3626c 100644 --- a/core/libs/prx/libSceLoginDialog/Export.cpp +++ b/core/libs/prx/libSceLoginDialog/Export.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -11,12 +12,29 @@ constexpr int COMMON_DIALOG_STATUS_NONE = 0; constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; -constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); -constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); -constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); -constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); -constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x81340001u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x81340002u); +constexpr int COMMON_DIALOG_ERROR_PARAM_INVALID = static_cast(0x81340003u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x81340005u); +constexpr int COMMON_DIALOG_ERROR_NO_SERVICE = static_cast(0x81340007u); constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; +constexpr std::uint32_t LOGIN_DIALOG_PARAM_SIZE = 0x40; +constexpr std::uint32_t LOGIN_DIALOG_SELECTOR_LIMIT = 2; +constexpr std::size_t LOGIN_DIALOG_FIRST_RESERVED_WORD = 0x2C / 4; + +bool ValidOpenParam(const void* param) { + if (param == nullptr) return false; + std::uint32_t size = 0; + std::memcpy(&size, param, sizeof(size)); + if (size != LOGIN_DIALOG_PARAM_SIZE) return false; + std::uint32_t words[LOGIN_DIALOG_PARAM_SIZE / sizeof(std::uint32_t)]; + std::memcpy(words, param, sizeof(words)); + if (words[1] >= LOGIN_DIALOG_SELECTOR_LIMIT) return false; + for (std::size_t i = LOGIN_DIALOG_FIRST_RESERVED_WORD; i < std::size(words); ++i) { + if (words[i] != 0) return false; + } + return true; +} std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; @@ -31,10 +49,10 @@ int APS5_VABI sceLoginDialogInitialize(void) { } int APS5_VABI sceLoginDialogOpen(const void* param) { + if (!ValidOpenParam(param)) return COMMON_DIALOG_ERROR_PARAM_INVALID; const int status = g_status.load(); if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; - if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } @@ -48,13 +66,12 @@ int APS5_VABI sceLoginDialogGetStatus(void) { } int APS5_VABI sceLoginDialogGetResult(void* result) { + if (result == nullptr) return COMMON_DIALOG_ERROR_PARAM_INVALID; const int status = g_status.load(); if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; - const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED; - std::memcpy(result, &canceled, sizeof(canceled)); - return 0; + const std::int32_t words[] = {COMMON_DIALOG_RESULT_USER_CANCELED, COMMON_DIALOG_ERROR_NO_SERVICE}; + std::memcpy(result, words, sizeof(words)); + return status == COMMON_DIALOG_STATUS_FINISHED ? 0 : COMMON_DIALOG_ERROR_BUSY; } int APS5_VABI sceLoginDialogClose(void) { diff --git a/core/libs/prx/libSceMbus/CMakeLists.txt b/core/libs/prx/libSceMbus/CMakeLists.txt index 7901f908fa..9de325ed3f 100644 --- a/core/libs/prx/libSceMbus/CMakeLists.txt +++ b/core/libs/prx/libSceMbus/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceMbus SHARED EXCLUDE_FROM_ALL Export.cpp) +add_library(libSceMbus SHARED EXCLUDE_FROM_ALL Export.cpp PendingExports.cpp) target_include_directories(libSceMbus PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceMbus) diff --git a/core/libs/prx/libSceMbus/PendingExports.cpp b/core/libs/prx/libSceMbus/PendingExports.cpp new file mode 100644 index 0000000000..c9296ae891 --- /dev/null +++ b/core/libs/prx/libSceMbus/PendingExports.cpp @@ -0,0 +1,580 @@ +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceMbusAcquireControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAcquireControlList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAcquireControlWithState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAcquireControlWithState2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAcquireControlWithStateFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandleByDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandleByUserId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandleByVidPid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandleWithModuleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAttachToApplicationByPid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusBindDeviceWithUserId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusChangeAppModuleState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusCheckDriverCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusConvertToLocalDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusConvertToLocalDeviceId2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusConvertToMbusDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAcquireControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAcquireControlWithState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAcquireControlWithState2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAcquireControlWithStateFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAddProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugCheckProcessResume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugDisableBgmForShellUi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetApplicationStartupInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetControlStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetInternalInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetPriorityInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugReenableBgmForShellUi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugReleaseControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugRemoveCameraAppModuleFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugResumeApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetApplicationFocusByAppId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetCameraAppModuleFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetControllerFocusByAppId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetOtherProcessExcludedAction() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugStartApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugStartApplication2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugStartApplicationNull() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSuspendApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugTerminateApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugTerminateProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDetachFromApplicationByPid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDisableBgmForShellUi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDisableChat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDisconnectDevice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpAppInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpHandleStatusInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpHandleStatusInfoAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpModuleStateInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpModuleStateInfoAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpPlayerInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpPortHandleInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpProcMediaInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpProcMediaInfoAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpUserMediaInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEnableChat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEnterAudioOutAutoAttenuation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventBusStatusChangeSubscribe() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventBusStatusChangeUnsubscribe() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventCallbackFuncsInit_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventCreate_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventReceive() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetControlStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceDescription() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceInfoByBusId_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceInfoByConditionForDeviceService() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceInfoByCondition_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceInfo_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetSimulatedBusUsableStatusByBusType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetSimulatedHandleStatusInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetSimulatedHandleStatusInfo2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetSparkState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusIsBgmProhibited() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusIsUsingDevice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusLeaveAudioOutAutoAttenuation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusNotifyBtDisconnection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusNotifyDriverEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusReenableBgmForShellUi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusRegisterDriver() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusReleaseControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusRemoveCameraAppModuleFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusRemoveHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusRemoveProxy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusResolveByDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusResolveByHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusResolveByPlayerId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusResolveByUserId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetAutoLoginMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetCameraAppModuleFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetDeviceFunctionState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetDuckingSetting() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetProxy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetRoutingMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetScratchData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetScratchDataUpdatedEventMask() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetSocialScreenAudioMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetTestFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetThreadParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetUsbPowerBlackList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetUserMuteStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetVolumeDown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusStartAudioOutAuxBroadcast() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusStartAudioOutSharePlay() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusStopAudioOutAuxBroadcast() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusStopAudioOutSharePlay() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusUnregisterDriver() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusUserLogin() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusUserLogout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceMouse/Export.cpp b/core/libs/prx/libSceMouse/Export.cpp index 7278fa33eb..121b9d2e6a 100644 --- a/core/libs/prx/libSceMouse/Export.cpp +++ b/core/libs/prx/libSceMouse/Export.cpp @@ -1,4 +1,5 @@ #include "prx/libSceMouse/include/MouseState.hpp" +#include "prx/libc/include/General.hpp" extern "C" { @@ -30,4 +31,54 @@ bool MouseIsOpen_nid_no_patch() { return Mouse::IsOpen(); } +int APS5_VABI sceMouseConnectPort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseDebugGetDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseDeviceOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseDisconnectDevice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseDisconnectPort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseGetDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseMbusInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseSetHandType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseSetPointerSpeed() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseSetProcessPrivilege() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceMove/Export.cpp b/core/libs/prx/libSceMove/Export.cpp index 6c2199ff94..df4c7fcf1a 100644 --- a/core/libs/prx/libSceMove/Export.cpp +++ b/core/libs/prx/libSceMove/Export.cpp @@ -1,33 +1,117 @@ #include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceUserService/UserService.hpp" + +namespace { + +constexpr int MOVE_OK = 0; +constexpr int MOVE_NO_CONTROLLER_CONNECTED = 1; +constexpr int MOVE_ERROR_NOT_INIT = static_cast(0x80EE0001); +constexpr int MOVE_ERROR_ALREADY_INIT = static_cast(0x80EE0002); +constexpr int MOVE_ERROR_INVALID_ARG = static_cast(0x80EE0003); +constexpr int32_t MOVE_FIRST_HANDLE = 0x30B0100; +constexpr int32_t MOVE_HANDLE_STEP = 0x100; + +std::mutex stateMutex; +bool initialized = false; +int32_t nextHandle = MOVE_FIRST_HANDLE; +struct Slot { + int32_t type; + int32_t index; +}; + +std::unordered_map openHandles; + +int checkHandle(const char* function, int32_t handle) { + std::lock_guard lock(stateMutex); + if (!initialized) return MOVE_ERROR_NOT_INIT; + if (!openHandles.contains(handle)) NotImplemented_nid_no_patch(function); + return MOVE_OK; +} + +} extern "C" { +int APS5_VABI sceMoveInit(void) { + std::lock_guard lock(stateMutex); + if (initialized) return MOVE_ERROR_ALREADY_INIT; + initialized = true; + return MOVE_OK; +} + +int APS5_VABI sceMoveTerm(void) { + std::lock_guard lock(stateMutex); + if (!initialized) return MOVE_ERROR_NOT_INIT; + initialized = false; + openHandles.clear(); + return MOVE_OK; +} + +int APS5_VABI sceMoveOpen(int32_t userId, int32_t type, int32_t index) { + std::lock_guard lock(stateMutex); + if (!initialized) return MOVE_ERROR_NOT_INIT; + if (userId != USER_SERVICE_INITIAL_USER_ID) NotImplemented_nid_no_patch(__func__); + for (const auto& [openHandle, slot] : openHandles) { + if (slot.type == type && slot.index == index) NotImplemented_nid_no_patch(__func__); + } + const int32_t handle = nextHandle; + nextHandle += MOVE_HANDLE_STEP; + openHandles.emplace(handle, Slot{type, index}); + return handle; +} + int APS5_VABI sceMoveClose(int32_t handle) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(stateMutex); + if (!initialized) return MOVE_ERROR_NOT_INIT; + if (openHandles.erase(handle) == 0) NotImplemented_nid_no_patch(__func__); + return MOVE_OK; } + +int APS5_VABI sceMoveGetDeviceInfo(int32_t handle, void* info) { + if (const int result = checkHandle(__func__, handle); result != MOVE_OK) return result; + if (!info) return MOVE_ERROR_INVALID_ARG; + return MOVE_NO_CONTROLLER_CONNECTED; +} + int APS5_VABI sceMoveGetExtensionPortInfo(int32_t handle, void* data) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (const int result = checkHandle(__func__, handle); result != MOVE_OK) return result; + if (!data) return MOVE_ERROR_INVALID_ARG; + return MOVE_NO_CONTROLLER_CONNECTED; } + int APS5_VABI sceMoveReadStateLatest(int32_t handle, void* data) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (const int result = checkHandle(__func__, handle); result != MOVE_OK) return result; + if (!data) return MOVE_ERROR_INVALID_ARG; + return MOVE_NO_CONTROLLER_CONNECTED; } -int APS5_VABI sceMoveReadStateRecent(int32_t handle, int64_t timestamp, void* data, int32_t* out_count) { - NotImplemented_nid_no_patch(__func__); - return 0; + +int APS5_VABI sceMoveReadStateRecent(int32_t handle, int64_t timestamp, void* data, int32_t* outCount) { + if (const int result = checkHandle(__func__, handle); result != MOVE_OK) return result; + if (timestamp < 0 || !data || !outCount) return MOVE_ERROR_INVALID_ARG; + return MOVE_NO_CONTROLLER_CONNECTED; } -int APS5_VABI sceMoveResetLightSphere(int32_t handle) { - NotImplemented_nid_no_patch(__func__); - return 0; + +int APS5_VABI sceMoveSetVibration(int32_t handle, uint8_t motor) { + (void)motor; + if (const int result = checkHandle(__func__, handle); result != MOVE_OK) return result; + return MOVE_NO_CONTROLLER_CONNECTED; } + int APS5_VABI sceMoveSetLightSphere(int32_t handle, uint8_t red, uint8_t green, uint8_t blue) { - NotImplemented_nid_no_patch(__func__); - return 0; + (void)red; + (void)green; + (void)blue; + if (const int result = checkHandle(__func__, handle); result != MOVE_OK) return result; + return MOVE_NO_CONTROLLER_CONNECTED; +} + +int APS5_VABI sceMoveResetLightSphere(int32_t handle) { + return checkHandle(__func__, handle); } } diff --git a/core/libs/prx/libSceMoveTracker/Export.cpp b/core/libs/prx/libSceMoveTracker/Export.cpp index d81f4a5e32..6c660b9787 100644 --- a/core/libs/prx/libSceMoveTracker/Export.cpp +++ b/core/libs/prx/libSceMoveTracker/Export.cpp @@ -1,10 +1,98 @@ -#include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceMoveTrackerCalibrateReset() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerCalibrateYaw() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerCameraUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerCaptureInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerCaptureTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerControllersUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerGetState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerGetVersion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerGetWorkingMemorySize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerPlayGetImages() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerPlayGetStates() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerPlayStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerPlayStep() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerRecordSave() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerRecordStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerRecordStop() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceNKWeb/Export.cpp b/core/libs/prx/libSceNKWeb/Export.cpp index d81f4a5e32..81401ab485 100644 --- a/core/libs/prx/libSceNKWeb/Export.cpp +++ b/core/libs/prx/libSceNKWeb/Export.cpp @@ -1,10 +1,18 @@ -#include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceNKWebInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNKWebTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 8fdc52dffe..bd4752bdb6 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestHeap.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/Time/include/Time.hpp" @@ -488,12 +490,24 @@ int receive_error(const Sock& socket) { } bool message_flags_supported(int flags, int accepted) { -#ifdef _WIN32 - accepted &= ~NET_MSG_DONTWAIT; -#endif return (flags & ~accepted) == 0; } +bool would_block(const Sock& socket, int flags, bool send) { +#ifdef _WIN32 + if ((flags & NET_MSG_DONTWAIT) == 0) return false; + WSAPOLLFD entry{}; + entry.fd = socket.native->value; + entry.events = send ? POLLWRNORM : POLLRDNORM; + return WSAPoll(&entry, 1, 0) == 0; +#else + static_cast(socket); + static_cast(flags); + static_cast(send); + return false; +#endif +} + int native_message_flags(int flags) { int native = (flags & NET_MSG_PEEK) ? MSG_PEEK : 0; #ifndef _WIN32 @@ -530,11 +544,11 @@ int wait_for_datagram(const Sock& socket, std::chrono::steady_clock::time_point return WSAPoll(&descriptor, 1, static_cast(std::min(remaining, INT_MAX))) == 0 ? NET_EAGAIN : 0; } -int wait_for_local_address(int s, const Sock& socket, bool empty) { +int wait_for_local_address(int s, const Sock& socket, bool empty, int flags) { if (socket.type == NET_SOCK_STREAM || socket.bound) return 0; const bool named = has_local_address(socket.native->value); if (!named && (socket.read_shut || empty)) return RECEIVE_NOTHING; - if (!named && socket.nonblock) return NET_EAGAIN; + if (!named && (socket.nonblock || (flags & NET_MSG_DONTWAIT) != 0)) return NET_EAGAIN; const auto deadline = socket.rcv_timeout_us > 0 ? std::chrono::steady_clock::now() + std::chrono::microseconds(socket.rcv_timeout_us) : std::chrono::steady_clock::time_point::max(); @@ -555,6 +569,46 @@ int wait_for_local_address(int s, const Sock& socket, bool empty) { } #endif +bool parse_ipv4_parts(const char* text, std::uint32_t& address) { + std::array parts{}; + std::size_t count = 0; + std::uint32_t value = 0; + for (;;) { + if (*text < '0' || *text > '9') return false; + std::uint32_t base = 10; + if (*text == '0') { + ++text; + if (*text == 'x' || *text == 'X') { + base = 16; + ++text; + } else if (*text != '9') { + base = 8; + } + } + value = 0; + for (;; ++text) { + const char c = *text; + if (c >= '0' && c <= '9') { + if (base == 8 && c >= '8') return false; + value = value * base + static_cast(c - '0'); + } else if (base == 16 && ((c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { + value = value * 16 + static_cast((c | 0x20) - 'a' + 10); + } else { + break; + } + } + if (*text != '.') break; + if (count == parts.size() || value > 0xff) return false; + parts[count++] = value; + ++text; + } + if (*text != '\0' && std::strchr(" \t\n\v\f\r", *text) == nullptr) return false; + if (value > (0xffffffffu >> (8 * count))) return false; + for (std::size_t i = 0; i < count; ++i) value |= parts[i] << (24 - 8 * i); + address = value; + return true; +} + } // namespace extern "C" { @@ -988,10 +1042,11 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { socket = it->second; } #ifdef _WIN32 - if (const int wait = wait_for_local_address(s, socket, len == 0)) return wait == RECEIVE_NOTHING ? 0 : fail(wait); + if (const int wait = wait_for_local_address(s, socket, len == 0, flags)) return wait == RECEIVE_NOTHING ? 0 : fail(wait); #endif const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); if (!destination.Open()) return fail(NET_EFAULT); + if (would_block(socket, flags, false)) return fail(NET_EAGAIN); const int native_flags = native_message_flags(flags); const int result = ::recv(socket.native->value, static_cast(buf), static_cast(len), native_flags); #ifdef _WIN32 @@ -1015,7 +1070,7 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* const GuestArena::HostWrite lengthOutput(from == nullptr ? nullptr : fromlen, from == nullptr ? 0 : sizeof(*fromlen)); if (!lengthOutput.Open()) return fail(NET_EFAULT); #ifdef _WIN32 - if (const int wait = wait_for_local_address(s, socket, len == 0)) { + if (const int wait = wait_for_local_address(s, socket, len == 0, flags)) { if (wait != RECEIVE_NOTHING) return fail(wait); if (from) *fromlen = 0; return 0; @@ -1029,6 +1084,7 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* if (!destination.Open()) return fail(NET_EFAULT); sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); + if (would_block(socket, flags, false)) return fail(NET_EAGAIN); const int native_flags = native_message_flags(flags); int result = ::recvfrom(socket.native->value, static_cast(buf), static_cast(len), native_flags, from ? reinterpret_cast(&peer) : nullptr, from ? &peer_length : nullptr); @@ -1051,6 +1107,7 @@ 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; } + if (would_block(socket, flags, true)) return fail(NET_EAGAIN); const int result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags)); return result >= 0 ? result : fail(native_error()); } @@ -1066,6 +1123,7 @@ 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; } + if (would_block(socket, flags, true)) return fail(NET_EAGAIN); int result; if (!to) { if (tolen != 0) return fail(NET_EINVAL); @@ -1124,7 +1182,7 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { } const bool datagram = socket.type != NET_SOCK_STREAM; #ifdef _WIN32 - if (const int wait = wait_for_local_address(s, socket, total == 0)) { + if (const int wait = wait_for_local_address(s, socket, total == 0, flags)) { if (wait != RECEIVE_NOTHING) return fail(wait); msg->flags = 0; if (msg->name) msg->name_length = 0; @@ -1132,6 +1190,7 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { return 0; } #endif + if (would_block(socket, flags, false)) return fail(NET_EAGAIN); std::vector buffer; try { buffer.resize(static_cast(total) + (datagram ? 1u : 0u)); @@ -1362,12 +1421,116 @@ int APS5_VABI sceNetGetpeername(int s, void* addr, uint32_t* addrlen) { return native_to_guest_address(native, addr, addrlen) ? 0 : fail(NET_EAFNOSUPPORT); } +constexpr int NET_SOCKINFO_F_SELF = 0x1; +constexpr int NET_SOCKINFO_F_NONBLOCK = 0x200000; +constexpr int NET_SOCKINFO_TIME_WAIT = 0x40; + +struct NetSockInfo { + char name[32]; + std::int32_t pid; + std::int32_t s; + std::int8_t socket_type; + std::int8_t policy; + std::int8_t priority; + std::int8_t reserved8; + std::int32_t recv_queue_length; + std::int32_t send_queue_length; + std::uint32_t local_adr; + std::uint32_t remote_adr; + std::uint16_t local_port; + std::uint16_t remote_port; + std::uint16_t local_vport; + std::uint16_t remote_vport; + std::int32_t state; + std::int32_t flags; + std::int32_t tx_bps; + std::int32_t rx_bps; + std::int32_t max_tx_bps; + std::int32_t max_rx_bps; + std::int32_t tx_vbps; + std::int32_t rx_vbps; + std::int32_t recv_buffer_size; + std::int32_t send_buffer_size; + std::int32_t reserved6[8]; + std::int32_t tx_drops; + std::int32_t rx_drops; + std::int32_t tx_wait; + std::int32_t reserved[2]; +}; +static_assert(sizeof(NetSockInfo) == 0xA0 && offsetof(NetSockInfo, s) == 0x24 && offsetof(NetSockInfo, local_adr) == 0x34 && + offsetof(NetSockInfo, local_port) == 0x3C && offsetof(NetSockInfo, recv_buffer_size) == 0x64); + +static int sock_buffer_size(const Sock& socket, bool send, std::int32_t& value) { + value = send ? socket.snd_buffer : socket.rcv_buffer; + if (value != 0) return 0; + NativeLength size = sizeof(value); + return ::getsockopt(socket.native->value, SOL_SOCKET, send ? SO_SNDBUF : SO_RCVBUF, reinterpret_cast(&value), &size) == 0 ? 0 : native_error(); +} + +static int fill_sock_info(int s, NetSockInfo& record) { + Sock socket; + { + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(s); + if (it == g_socks.end()) return NET_EBADF; + socket = it->second; + } + if (socket.family != NET_AF_INET) return NET_EAFNOSUPPORT; + sockaddr_in local{}; + NativeLength length = sizeof(local); + if (::getsockname(socket.native->value, reinterpret_cast(&local), &length) != 0) { +#ifdef _WIN32 + if (WSAGetLastError() != WSAEINVAL) return native_error(); +#else + return native_error(); +#endif + } + sockaddr_in remote{}; + length = sizeof(remote); + if (::getpeername(socket.native->value, reinterpret_cast(&remote), &length) != 0) { + const int error = native_error(); + if (error != NET_ENOTCONN) return error; + } + record = {}; + if (const int error = sock_buffer_size(socket, false, record.recv_buffer_size)) return error; + if (const int error = sock_buffer_size(socket, true, record.send_buffer_size)) return error; + record.s = s; + record.socket_type = static_cast(socket.type); + record.local_adr = local.sin_addr.s_addr; + record.remote_adr = remote.sin_addr.s_addr; + record.local_port = local.sin_port; + record.remote_port = remote.sin_port; + record.flags = NET_SOCKINFO_F_SELF | (socket.nonblock ? NET_SOCKINFO_F_NONBLOCK : 0); + return 0; +} + int APS5_VABI sceNetGetSockInfo(int s, void* info, int n, int flags) { - (void)info; - (void)n; - (void)flags; - std::lock_guard lk(g_mutex); - return g_socks.count(s) != 0 ? 0 : fail(NET_EBADF); + if ((flags & ~NET_SOCKINFO_TIME_WAIT) != 0) return fail(NET_EINVAL); + std::vector listed; + if (s < 0) { + std::lock_guard lk(g_mutex); + for (const auto& [descriptor, socket] : g_socks) { + if (socket.family == NET_AF_INET) listed.push_back(descriptor); + } + } + if (!info) return s < 0 ? static_cast(listed.size()) : fail(NET_EINVAL); + if (n < 1) return fail(NET_EINVAL); + NetSockInfo record; + if (s >= 0) { + if (const int error = fill_sock_info(s, record)) return fail(error); + std::memcpy(info, &record, sizeof(record)); + return 1; + } + int written = 0; + for (const int descriptor : listed) { + if (written == n) break; + const int error = fill_sock_info(descriptor, record); + if (error == NET_EBADF || error == NET_EAFNOSUPPORT) continue; + if (error != 0) return fail(error); + std::memcpy(static_cast(info) + static_cast(written) * sizeof(record), &record, sizeof(record)); + ++written; + } + return written; } int APS5_VABI sceNetEpollCreate(const char* name, int flags) { @@ -1745,23 +1908,15 @@ int APS5_VABI sceNetResolverStartNtoaMultipleRecordsEx(int rid, const char* host return lookup_records(rid, hostname, info, flags, __func__); } -int APS5_VABI sceNetResolverStartAton(int rid, const void* addr, char* hostname, int len, int timeout, int retry, int flags) { - (void)timeout; - (void)retry; - (void)flags; - if (!addr || !hostname || len <= 0) return fail(NET_EINVAL); +int reverse_lookup(int rid, const sockaddr* address, NativeLength addressLength, char* hostname, int len, const char* func) { if (!initialize_sockets()) return fail(5); std::uint64_t generation = 0; if (const int error = begin_lookup(rid, &Resolver::abort_aton, generation)) return error; - sockaddr_in address{}; - address.sin_family = AF_INET; - std::memcpy(&address.sin_addr, addr, sizeof(address.sin_addr)); std::vector name(static_cast(len)); - const int result = ::getnameinfo(reinterpret_cast(&address), sizeof(address), name.data(), - static_cast(len), nullptr, 0, NI_NAMEREQD); + const int result = ::getnameinfo(address, addressLength, name.data(), static_cast(len), nullptr, 0, NI_NAMEREQD); if (result != 0) { set_errno(result == EAI_AGAIN ? NET_ETIMEDOUT : NET_ENOENT); - log_soft(__func__, "host reverse DNS lookup failed"); + log_soft(func, "host reverse DNS lookup failed"); return end_lookup(rid, generation, NET_ERROR_RESOLVER_ENODNS); } if (const int error = end_lookup(rid, generation, 0)) return error; @@ -1769,6 +1924,28 @@ int APS5_VABI sceNetResolverStartAton(int rid, const void* addr, char* hostname, return 0; } +int APS5_VABI sceNetResolverStartAton(int rid, const void* addr, char* hostname, int len, int timeout, int retry, int flags) { + (void)timeout; + (void)retry; + (void)flags; + if (!addr || !hostname || len <= 0) return fail(NET_EINVAL); + sockaddr_in address{}; + address.sin_family = AF_INET; + std::memcpy(&address.sin_addr, addr, sizeof(address.sin_addr)); + return reverse_lookup(rid, reinterpret_cast(&address), sizeof(address), hostname, len, __func__); +} + +int APS5_VABI sceNetResolverStartAton6(int rid, const void* addr, char* hostname, int len, int timeout, int retry, int flags) { + (void)timeout; + (void)retry; + if (flags != 0) throw std::runtime_error("sceNetResolverStartAton6: flags " + std::to_string(flags) + " are not supported"); + if (!addr || !hostname || len <= 0) return fail(NET_EINVAL); + sockaddr_in6 address{}; + address.sin6_family = AF_INET6; + std::memcpy(&address.sin6_addr, addr, sizeof(address.sin6_addr)); + return reverse_lookup(rid, reinterpret_cast(&address), sizeof(address), hostname, len, __func__); +} + int APS5_VABI sceNetResolverGetError(int rid, int* status) { if (!status) return fail(NET_EINVAL); std::lock_guard lk(g_mutex); @@ -1801,8 +1978,8 @@ int APS5_VABI sceNetShowIfconfig() { extern const std::uint8_t in6addr_any_nid_postfix[16] = {}; extern const std::uint8_t in6addr_loopback_nid_postfix[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; -int APS5_VABI sceNetClearDnsCache(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNetClearDnsCache(int flags) { + if (flags != 0) throw std::runtime_error("sceNetClearDnsCache: flags " + std::to_string(flags) + " are not supported"); return 0; } @@ -1846,11 +2023,6 @@ int APS5_VABI sceNetShowRouteWithMemory(void) { return 0; } -int APS5_VABI sceNetResolverStartAton6(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNetResolverConnectAbort(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1966,9 +2138,14 @@ int APS5_VABI sceNetGetIfListOnce(void) { return 0; } -int APS5_VABI sceNetGetIfName(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +const char* APS5_VABI sceNetGetIfName(std::uint32_t index) { + static constexpr std::array names = { + "lo0", "eth0", "eth1", "dbg0", "wlan0", "wlan1", "gbe0", "bt0", "phone0", "pppoe0"}; + if (index >= names.size()) { + set_errno(NET_EINVAL); + return ""; + } + return names[index]; } int APS5_VABI sceNetGetIfnameNumList(void) { @@ -1981,8 +2158,12 @@ int APS5_VABI sceNetGetNameToIndex(void) { return 0; } -int APS5_VABI sceNetGetRandom(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNetGetRandom(std::uint32_t* output) { + if (output == nullptr) return fail(NET_EINVAL); + static std::mutex mutex; + static std::random_device device; + const std::lock_guard lock(mutex); + *output = static_cast(device()); return 0; } @@ -2010,9 +2191,21 @@ int APS5_VABI sceNetInitParam(void) { return 0; } -int APS5_VABI sceNetInetPtonEx(int af, const char* src, void* dst, int flags) { - if (flags != 0) throw std::runtime_error("sceNetInetPtonEx: flags " + std::to_string(flags) + " are not supported"); - return sceNetInetPton(af, src, dst); +int APS5_VABI sceNetInetPtonEx(int af, const char* src, void* dst, int) { + if (af != NET_AF_INET) { + return sceNetInetPton(af, src, dst); + } + if (src == nullptr || dst == nullptr) { + return fail(NET_EINVAL); + } + std::uint32_t address = 0; + if (!parse_ipv4_parts(src, address)) { + return 0; + } + const std::array bytes{static_cast(address >> 24), static_cast(address >> 16), + static_cast(address >> 8), static_cast(address)}; + std::memcpy(dst, bytes.data(), bytes.size()); + return 1; } int APS5_VABI sceNetIoctl(void) { @@ -2020,14 +2213,25 @@ int APS5_VABI sceNetIoctl(void) { return 0; } -int APS5_VABI sceNetMemoryAllocate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void* APS5_VABI sceNetMemoryAllocate(std::int64_t size, int flags) { + constexpr int zeroFill = 2; + if ((flags & ~zeroFill) != 0) + throw std::runtime_error("sceNetMemoryAllocate: flags " + std::to_string(flags) + " are not supported"); + if (size <= 0) return nullptr; + void* memory = nullptr; + try { + memory = GuestHeap::GuestHeapAllocate_nid_postfix(static_cast(size)); + } catch (const std::bad_alloc&) { + return nullptr; + } catch (const std::length_error&) { + return nullptr; + } + if ((flags & zeroFill) != 0) std::memset(memory, 0, static_cast(size)); + return memory; } -int APS5_VABI sceNetMemoryFree(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI sceNetMemoryFree(void* memory) { + GuestHeap::GuestHeapFree_nid_postfix(memory); } int APS5_VABI sceNetSetDnsInfoToKernel(void) { @@ -2650,4 +2854,9 @@ int APS5_VABI sceNetEmulationGet(void) { return 0; } +int APS5_VABI sceNetEtherStrton(const char* str, void* address) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index dbc3d4c09a..1e11aadff5 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -511,4 +511,359 @@ int APS5_VABI sceNetCtlUnregisterCallbackV6(int cid) { return UnregisterCallback(g_callbacksV6, cid); } +int APS5_VABI sceNetCtlRegisterCallbackForNpToolkit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlRegisterCallbackIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlRegisterCallbackForLibIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlUnregisterCallbackForNpToolkit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlUnregisterCallbackIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlUnregisterCallbackForLibIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlCheckCallbackForNpToolkit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlCheckCallbackForLibIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlClearEventIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlClearEventForNpToolkit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlClearEventForLibIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetResultIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetStateIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetState2IpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetInfoIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetEtherLinkMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetWifiType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlConnectIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlConnectWithRetryIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlConnectConfIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlDisconnectIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlScanIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoBssidIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoByBssidIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoForSsidScanIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoBssidForSsidListScanIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoForSsidListScanIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlSetErrorNotificationEnabledIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlRegisterCallbackV6IpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlUnregisterCallbackV6IpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetResultV6(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetResultV6IpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetStateV6IpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetInfoV6IpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetNatInfoIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlSetStunWithPaddingFlagIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlUnsetStunWithPaddingFlagIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlIsBandwidthManagementEnabledIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlDisableBandwidthManagementIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlEnableBandwidthManagementIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetBandwidthInfoIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetNetEvConfigInfoIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetConnectionElapsedTimeIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpCheckCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpClearEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpRegisterCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpUnregisterCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpGetResult(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpGetState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpGetInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApCpStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApAppStartWithRetryPid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApAppStartWithRetry(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpStartWithRetry(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpStartConf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApCpStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApAppInitWpaKey(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApAppInitWpaKeyForQa(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRestart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweCheckCallbackIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweClearEventIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweRegisterCallbackIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweUnregisterCallbackIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweGetInfoIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweStartInternetConnectionTestIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweFinishInternetConnectionTestIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweStartInternetConnectionTestBandwidthTestIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweSetInternetConnectionTestResultIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetWifiInfoIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNetCtlAp/Export.cpp b/core/libs/prx/libSceNetCtlAp/Export.cpp index 8791105967..d0bcb19845 100644 --- a/core/libs/prx/libSceNetCtlAp/Export.cpp +++ b/core/libs/prx/libSceNetCtlAp/Export.cpp @@ -1,72 +1,130 @@ +#include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int NET_CTL_AP_ERROR_NOT_STARTED = static_cast(0x8041298F); +constexpr int NET_CTL_AP_ERROR_INVALID_TYPE = static_cast(0x80412990); +constexpr int NET_CTL_AP_ERROR_INVALID_ADDR = static_cast(0x80412991); +constexpr int NET_CTL_AP_ERROR_NOT_INITIALIZED = static_cast(0x80412994); +constexpr int NET_CTL_AP_ERROR_CALLBACK_MAX = static_cast(0x80412995); +constexpr int NET_CTL_AP_ERROR_INVALID_ID = static_cast(0x80412996); +constexpr int NET_CTL_AP_ERROR_ID_NOT_FOUND = static_cast(0x80412997); +constexpr int NET_CTL_AP_ERROR_INVALID_SIZE = static_cast(0x80412998); +constexpr int NET_CTL_AP_STATE_STOPPED = 0; +constexpr int NET_CTL_AP_EVENT_TYPE_FIRST = 1; +constexpr int NET_CTL_AP_EVENT_TYPE_LAST = 4; +constexpr std::size_t NET_CTL_AP_INFO_SIZE = 152; +constexpr std::size_t NET_CTL_AP_CONNECT_INFO_SIZE = 184; +constexpr int MAX_CALLBACKS = 8; + +using NetCtlApCallback = void(APS5_VABI*)(int, void*); + +struct CallbackSlot { + NetCtlApCallback func = nullptr; + void* arg = nullptr; +}; + +std::mutex g_lock; +bool g_initialized = false; +CallbackSlot g_callbacks[MAX_CALLBACKS] = {}; + +bool ValidEventType(int eventType) { + return eventType >= NET_CTL_AP_EVENT_TYPE_FIRST && eventType <= NET_CTL_AP_EVENT_TYPE_LAST; +} + +} + extern "C" { int APS5_VABI sceNetCtlApInit() { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_lock); + if (g_initialized) throw std::runtime_error("sceNetCtlApInit: a second initialization is not implemented"); + g_initialized = true; return 0; } void APS5_VABI sceNetCtlApTerm() { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_lock); + g_initialized = false; + for (auto& slot : g_callbacks) slot = {}; } int APS5_VABI sceNetCtlApCheckCallback() { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_lock); + return g_initialized ? 0 : NET_CTL_AP_ERROR_NOT_INITIALIZED; } int APS5_VABI sceNetCtlApClearEvent(int eventType) { - (void)eventType; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + return ValidEventType(eventType) ? 0 : NET_CTL_AP_ERROR_INVALID_TYPE; } -int APS5_VABI sceNetCtlApRegisterCallback(void (*func)(int, void*), void* arg, int* cid) { - (void)func; - (void)arg; - (void)cid; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNetCtlApRegisterCallback(NetCtlApCallback func, void* arg, int* cid) { + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + if (!func || !cid) return NET_CTL_AP_ERROR_INVALID_ADDR; + for (int index = 0; index < MAX_CALLBACKS; ++index) { + if (!g_callbacks[index].func) { + g_callbacks[index] = {func, arg}; + *cid = index; + return 0; + } + } + return NET_CTL_AP_ERROR_CALLBACK_MAX; } int APS5_VABI sceNetCtlApUnregisterCallback(int cid) { - (void)cid; - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + if (cid < 0 || cid >= MAX_CALLBACKS) return NET_CTL_AP_ERROR_INVALID_ID; + if (!g_callbacks[cid].func) return NET_CTL_AP_ERROR_ID_NOT_FOUND; + g_callbacks[cid] = {}; return 0; } int APS5_VABI sceNetCtlApGetResult(int eventType, int* errorCode) { - (void)eventType; - (void)errorCode; - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + if (!ValidEventType(eventType)) return NET_CTL_AP_ERROR_INVALID_TYPE; + if (!errorCode) return NET_CTL_AP_ERROR_INVALID_ADDR; + *errorCode = 0; return 0; } int APS5_VABI sceNetCtlApGetInfo(void* info) { - (void)info; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + if (!info) return NET_CTL_AP_ERROR_INVALID_ADDR; + if (*static_cast(info) != NET_CTL_AP_INFO_SIZE) return NET_CTL_AP_ERROR_INVALID_SIZE; + return NET_CTL_AP_ERROR_NOT_STARTED; } int APS5_VABI sceNetCtlApGetState(int* state) { - (void)state; - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + if (!state) return NET_CTL_AP_ERROR_INVALID_ADDR; + *state = NET_CTL_AP_STATE_STOPPED; return 0; } int APS5_VABI sceNetCtlApStop() { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_lock); + return g_initialized ? NET_CTL_AP_ERROR_NOT_STARTED : NET_CTL_AP_ERROR_NOT_INITIALIZED; } int APS5_VABI sceNetCtlApGetConnectInfo(std::uint8_t* optStr, void* connectInfo) { (void)optStr; - (void)connectInfo; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + if (!connectInfo) return NET_CTL_AP_ERROR_INVALID_ADDR; + if (*static_cast(connectInfo) != NET_CTL_AP_CONNECT_INFO_SIZE) return NET_CTL_AP_ERROR_INVALID_SIZE; + return NET_CTL_AP_ERROR_NOT_STARTED; } } diff --git a/core/libs/prx/libSceNgs2/CMakeLists.txt b/core/libs/prx/libSceNgs2/CMakeLists.txt index 6d09bbbe29..41b6cfe96a 100644 --- a/core/libs/prx/libSceNgs2/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2/CMakeLists.txt @@ -1,5 +1,6 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/Ngs2Library.cmake) add_sce_ngs2_library(libSceNgs2) +target_sources(libSceNgs2 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/PendingExports.cpp) if(BUILD_TESTING) add_test_executable(ngs2_core_render_tests ${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/tests/Ngs2Render.cpp) diff --git a/core/libs/prx/libSceNgs2/PendingExports.cpp b/core/libs/prx/libSceNgs2/PendingExports.cpp new file mode 100644 index 0000000000..992e9b2449 --- /dev/null +++ b/core/libs/prx/libSceNgs2/PendingExports.cpp @@ -0,0 +1,57 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNgs2CustomRackGetModuleInfo(std::uintptr_t rackHandle, std::uint32_t moduleIndex, void* outInfo, std::size_t infoSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2FftInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2FftProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2FftQuerySize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2GetWaveformFrameInfo(void* format, std::uint32_t* outFrameSize, std::uint32_t* outNumFrameSamples, std::uint32_t* outUnitsPerFrame, std::uint32_t* outNumDelaySamples) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2ParseWaveformUser(std::uintptr_t handler, std::uintptr_t userData, void* outInfo) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2RackGetUserData(std::uintptr_t rackHandle, std::uintptr_t* outUserData) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2RackSetUserData(std::uintptr_t rackHandle, std::uintptr_t userData) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2SystemEnumHandles(std::uintptr_t* outHandles, std::uint32_t maxHandles) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2SystemEnumRackHandles(std::uintptr_t systemHandle, std::uintptr_t* outHandles, std::uint32_t maxHandles) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2VoiceGetMatrixInfo(std::uintptr_t voiceHandle, std::uint32_t matrixId, void* outInfo, std::size_t infoSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2VoiceGetOwner(std::uintptr_t voiceHandle, std::uintptr_t* outOwnerRack, std::uint32_t* outVoiceIndex) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpAuth/Export.cpp b/core/libs/prx/libSceNpAuth/Export.cpp index 86734f9a5a..2e9ece6558 100644 --- a/core/libs/prx/libSceNpAuth/Export.cpp +++ b/core/libs/prx/libSceNpAuth/Export.cpp @@ -103,6 +103,14 @@ int APS5_VABI sceNpAuthGetIdTokenV3(int req_id, const void* param, void* id_toke return SCE_NP_ERROR_SIGNED_OUT; } +int APS5_VABI sceNpAuthGetAuthorizationCode(int req_id, const void* param, void* auth_code) { + return sceNpAuthGetAuthorizationCodeV3(req_id, param, auth_code, nullptr); +} + +int APS5_VABI sceNpAuthGetIdToken(int req_id, const void* param, void* id_token) { + return sceNpAuthGetIdTokenV3(req_id, param, id_token); +} + int APS5_VABI sceNpAuthPollAsync(int req_id, int* result) { std::lock_guard lock(g_requestMutex); auto it = g_requests.find(req_id); @@ -136,4 +144,14 @@ int APS5_VABI sceNpAuthWaitAsync(int req_id, int* result) { return NP_POLL_ASYNC_FINISHED; } +int APS5_VABI sceNpAuthGetAuthorizationCodeA(int req_id, const void* param, void* auth_code, int* issuer_id) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpAuthGetIdTokenA(int req_id, const void* param, void* token) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpCommerce/Export.cpp b/core/libs/prx/libSceNpCommerce/Export.cpp index 39fecd0fc0..2fd73aeb90 100644 --- a/core/libs/prx/libSceNpCommerce/Export.cpp +++ b/core/libs/prx/libSceNpCommerce/Export.cpp @@ -84,4 +84,9 @@ int APS5_VABI sceNpCommerceDialogOpen2(void) { return 0; } +int APS5_VABI sceNpCommerceDialogInitializeInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp index 8bf9b62653..b2b6ff6b1c 100644 --- a/core/libs/prx/libSceNpCppWebApi/Export.cpp +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -1427,441 +1427,368 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_15DefaultResponseENS2 return 0; } -APS5_EXPORT("+gSMGXHgrPU", sceNpCppWebApiUnknown00); -int APS5_VABI sceNpCppWebApiUnknown00(void) { - NotImplemented_nid_no_patch("+gSMGXHgrPU"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEED2Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("-XNfwZTFkWw", sceNpCppWebApiUnknown01); -int APS5_VABI sceNpCppWebApiUnknown01(void) { - NotImplemented_nid_no_patch("-XNfwZTFkWw"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEED2Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("0f5+eBcgB14", sceNpCppWebApiUnknown02); -int APS5_VABI sceNpCppWebApiUnknown02(void) { - NotImplemented_nid_no_patch("0f5+eBcgB14"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEC1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("1iq5Jtw4jVs", sceNpCppWebApiUnknown03); -int APS5_VABI sceNpCppWebApiUnknown03(void) { - NotImplemented_nid_no_patch("1iq5Jtw4jVs"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEdeEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("1wpCXuFzH10", sceNpCppWebApiUnknown04); -int APS5_VABI sceNpCppWebApiUnknown04(void) { - NotImplemented_nid_no_patch("1wpCXuFzH10"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEptEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("4AFThLW28xQ", sceNpCppWebApiUnknown05); -int APS5_VABI sceNpCppWebApiUnknown05(void) { - NotImplemented_nid_no_patch("4AFThLW28xQ"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("8-9Y4oS+OuE", sceNpCppWebApiUnknown06); -int APS5_VABI sceNpCppWebApiUnknown06(void) { - NotImplemented_nid_no_patch("8-9Y4oS+OuE"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V57ProductEEEEEEptEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("8dWkWjo5EeE", sceNpCppWebApiUnknown07); -int APS5_VABI sceNpCppWebApiUnknown07(void) { - NotImplemented_nid_no_patch("8dWkWjo5EeE"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V57ProductEEEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("BHvfIGdarBQ", sceNpCppWebApiUnknown08); -int APS5_VABI sceNpCppWebApiUnknown08(void) { - NotImplemented_nid_no_patch("BHvfIGdarBQ"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V53SkuEEEEEEptEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("Fxxr5lYBfl4", sceNpCppWebApiUnknown09); -int APS5_VABI sceNpCppWebApiUnknown09(void) { - NotImplemented_nid_no_patch("Fxxr5lYBfl4"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V53SkuEEEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("M1LnjRscx-w", sceNpCppWebApiUnknown10); -int APS5_VABI sceNpCppWebApiUnknown10(void) { - NotImplemented_nid_no_patch("M1LnjRscx-w"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_17TitleCloudStorage2V110DataStatusEEEEEEdeEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("PsX657reZeo", sceNpCppWebApiUnknown11); -int APS5_VABI sceNpCppWebApiUnknown11(void) { - NotImplemented_nid_no_patch("PsX657reZeo"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_17TitleCloudStorage2V110DataStatusEEEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("RvH9AZ5rCkQ", sceNpCppWebApiUnknown12); -int APS5_VABI sceNpCppWebApiUnknown12(void) { - NotImplemented_nid_no_patch("RvH9AZ5rCkQ"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEC1ERS7_(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("SR3BKonD8yk", sceNpCppWebApiUnknown13); -int APS5_VABI sceNpCppWebApiUnknown13(void) { - NotImplemented_nid_no_patch("SR3BKonD8yk"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEptEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("TSoQhYKyq5g", sceNpCppWebApiUnknown14); -int APS5_VABI sceNpCppWebApiUnknown14(void) { - NotImplemented_nid_no_patch("TSoQhYKyq5g"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V59ContainerEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("TgsJjQqp+2k", sceNpCppWebApiUnknown15); -int APS5_VABI sceNpCppWebApiUnknown15(void) { - NotImplemented_nid_no_patch("TgsJjQqp+2k"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V57ProductEEC1ERS7_(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("UHQxE3HhTXA", sceNpCppWebApiUnknown16); -int APS5_VABI sceNpCppWebApiUnknown16(void) { - NotImplemented_nid_no_patch("UHQxE3HhTXA"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V57ProductEEptEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("ZN-glpk0Rug", sceNpCppWebApiUnknown17); -int APS5_VABI sceNpCppWebApiUnknown17(void) { - NotImplemented_nid_no_patch("ZN-glpk0Rug"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V57ProductEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("ZQzXwokCnUE", sceNpCppWebApiUnknown18); -int APS5_VABI sceNpCppWebApiUnknown18(void) { - NotImplemented_nid_no_patch("ZQzXwokCnUE"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEptEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("ZwjmbSr4Cxc", sceNpCppWebApiUnknown19); -int APS5_VABI sceNpCppWebApiUnknown19(void) { - NotImplemented_nid_no_patch("ZwjmbSr4Cxc"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEC1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("b7leY-LNVnI", sceNpCppWebApiUnknown20); -int APS5_VABI sceNpCppWebApiUnknown20(void) { - NotImplemented_nid_no_patch("b7leY-LNVnI"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEptEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("c3J9K9XbQqE", sceNpCppWebApiUnknown21); -int APS5_VABI sceNpCppWebApiUnknown21(void) { - NotImplemented_nid_no_patch("c3J9K9XbQqE"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("cSLbQUQO2Ns", sceNpCppWebApiUnknown22); -int APS5_VABI sceNpCppWebApiUnknown22(void) { - NotImplemented_nid_no_patch("cSLbQUQO2Ns"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V15OwnerEEptEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("cqOxjL0ZfFA", sceNpCppWebApiUnknown23); -int APS5_VABI sceNpCppWebApiUnknown23(void) { - NotImplemented_nid_no_patch("cqOxjL0ZfFA"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V15OwnerEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("d1eEjWR60wk", sceNpCppWebApiUnknown24); -int APS5_VABI sceNpCppWebApiUnknown24(void) { - NotImplemented_nid_no_patch("d1eEjWR60wk"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEEneERKS9_(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("epwr+cBCIFs", sceNpCppWebApiUnknown25); -int APS5_VABI sceNpCppWebApiUnknown25(void) { - NotImplemented_nid_no_patch("epwr+cBCIFs"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEEdeEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("eruduJ0KrT0", sceNpCppWebApiUnknown26); -int APS5_VABI sceNpCppWebApiUnknown26(void) { - NotImplemented_nid_no_patch("eruduJ0KrT0"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEEppEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("fG06a38iao8", sceNpCppWebApiUnknown27); -int APS5_VABI sceNpCppWebApiUnknown27(void) { - NotImplemented_nid_no_patch("fG06a38iao8"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEEneERKS9_(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("jfpSx14AeLc", sceNpCppWebApiUnknown28); -int APS5_VABI sceNpCppWebApiUnknown28(void) { - NotImplemented_nid_no_patch("jfpSx14AeLc"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEEdeEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("kBxwE4YfbIM", sceNpCppWebApiUnknown29); -int APS5_VABI sceNpCppWebApiUnknown29(void) { - NotImplemented_nid_no_patch("kBxwE4YfbIM"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEEppEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("kz2Z38yTLq0", sceNpCppWebApiUnknown30); -int APS5_VABI sceNpCppWebApiUnknown30(void) { - NotImplemented_nid_no_patch("kz2Z38yTLq0"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("lB8driFKaoU", sceNpCppWebApiUnknown31); -int APS5_VABI sceNpCppWebApiUnknown31(void) { - NotImplemented_nid_no_patch("lB8driFKaoU"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERSB_(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("mCsV7izOkjY", sceNpCppWebApiUnknown32); -int APS5_VABI sceNpCppWebApiUnknown32(void) { - NotImplemented_nid_no_patch("mCsV7izOkjY"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("n1yCLMxpseQ", sceNpCppWebApiUnknown33); -int APS5_VABI sceNpCppWebApiUnknown33(void) { - NotImplemented_nid_no_patch("n1yCLMxpseQ"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("nL9vSVq-29s", sceNpCppWebApiUnknown34); -int APS5_VABI sceNpCppWebApiUnknown34(void) { - NotImplemented_nid_no_patch("nL9vSVq-29s"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("nmz5JYKcMfY", sceNpCppWebApiUnknown35); -int APS5_VABI sceNpCppWebApiUnknown35(void) { - NotImplemented_nid_no_patch("nmz5JYKcMfY"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("oHRl7a+zdMU", sceNpCppWebApiUnknown36); -int APS5_VABI sceNpCppWebApiUnknown36(void) { - NotImplemented_nid_no_patch("oHRl7a+zdMU"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("pSpO3hPNv64", sceNpCppWebApiUnknown37); -int APS5_VABI sceNpCppWebApiUnknown37(void) { - NotImplemented_nid_no_patch("pSpO3hPNv64"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("q4UFICay6Hk", sceNpCppWebApiUnknown38); -int APS5_VABI sceNpCppWebApiUnknown38(void) { - NotImplemented_nid_no_patch("q4UFICay6Hk"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("qiD2PU2Jstc", sceNpCppWebApiUnknown39); -int APS5_VABI sceNpCppWebApiUnknown39(void) { - NotImplemented_nid_no_patch("qiD2PU2Jstc"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("tFe964qzGEM", sceNpCppWebApiUnknown40); -int APS5_VABI sceNpCppWebApiUnknown40(void) { - NotImplemented_nid_no_patch("tFe964qzGEM"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEEEixEm(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("tHaO36kincQ", sceNpCppWebApiUnknown41); -int APS5_VABI sceNpCppWebApiUnknown41(void) { - NotImplemented_nid_no_patch("tHaO36kincQ"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEEE4sizeEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("umm5m+mXiZs", sceNpCppWebApiUnknown42); -int APS5_VABI sceNpCppWebApiUnknown42(void) { - NotImplemented_nid_no_patch("umm5m+mXiZs"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEE5beginEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("uwhztB49KOQ", sceNpCppWebApiUnknown43); -int APS5_VABI sceNpCppWebApiUnknown43(void) { - NotImplemented_nid_no_patch("uwhztB49KOQ"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEE3endEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("w4K4nTYwhVE", sceNpCppWebApiUnknown44); -int APS5_VABI sceNpCppWebApiUnknown44(void) { - NotImplemented_nid_no_patch("w4K4nTYwhVE"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V53SkuEEEE4sizeEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("wFcm6bgWB9Q", sceNpCppWebApiUnknown45); -int APS5_VABI sceNpCppWebApiUnknown45(void) { - NotImplemented_nid_no_patch("wFcm6bgWB9Q"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEE5beginEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("wp8+c84G5Xw", sceNpCppWebApiUnknown46); -int APS5_VABI sceNpCppWebApiUnknown46(void) { - NotImplemented_nid_no_patch("wp8+c84G5Xw"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEE3endEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("zjqTmP0ST9A", sceNpCppWebApiUnknown47); -int APS5_VABI sceNpCppWebApiUnknown47(void) { - NotImplemented_nid_no_patch("zjqTmP0ST9A"); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V59Container11getChildrenEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("zzO8ZGJ74ng", sceNpCppWebApiUnknown48); -int APS5_VABI sceNpCppWebApiUnknown48(void) { - NotImplemented_nid_no_patch("zzO8ZGJ74ng"); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainerC1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("12wY179+CE8", sceNpCppWebApiUnknown49); -int APS5_VABI sceNpCppWebApiUnknown49(void) { - NotImplemented_nid_no_patch("12wY179+CE8"); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer10initializeEPNS1_6Common10LibContextE(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("3bjEFf8OhTs", sceNpCppWebApiUnknown50); -int APS5_VABI sceNpCppWebApiUnknown50(void) { - NotImplemented_nid_no_patch("3bjEFf8OhTs"); +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer13isInitializedEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("6FK4IOnANkc", sceNpCppWebApiUnknown51); -int APS5_VABI sceNpCppWebApiUnknown51(void) { - NotImplemented_nid_no_patch("6FK4IOnANkc"); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer15setserviceLabelEi(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("EJAOHYWUki4", sceNpCppWebApiUnknown52); -int APS5_VABI sceNpCppWebApiUnknown52(void) { - NotImplemented_nid_no_patch("EJAOHYWUki4"); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer9terminateEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("FlagLhjAEmc", sceNpCppWebApiUnknown53); -int APS5_VABI sceNpCppWebApiUnknown53(void) { - NotImplemented_nid_no_patch("FlagLhjAEmc"); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainerD1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("G3h-NDHnyW4", sceNpCppWebApiUnknown54); -int APS5_VABI sceNpCppWebApiUnknown54(void) { - NotImplemented_nid_no_patch("G3h-NDHnyW4"); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi12getContainerEiRKNS4_23ParameterToGetContainerERNS1_6Common11TransactionINS8_12IntrusivePtrINS8_6VectorINSA_INS3_9ContainerEEEEEEENSA_INS8_18ResponseHeaderBaseEEEEE(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("H0XjNhflED4", sceNpCppWebApiUnknown55); -int APS5_VABI sceNpCppWebApiUnknown55(void) { - NotImplemented_nid_no_patch("H0XjNhflED4"); +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product8getLabelEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("IbWx007Acn4", sceNpCppWebApiUnknown56); -int APS5_VABI sceNpCppWebApiUnknown56(void) { - NotImplemented_nid_no_patch("IbWx007Acn4"); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V57Product7getSkusEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("Is+nI7Hq9jU", sceNpCppWebApiUnknown57); -int APS5_VABI sceNpCppWebApiUnknown57(void) { - NotImplemented_nid_no_patch("Is+nI7Hq9jU"); +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product7getTypeEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("Kc4x2uy0FFk", sceNpCppWebApiUnknown58); -int APS5_VABI sceNpCppWebApiUnknown58(void) { - NotImplemented_nid_no_patch("Kc4x2uy0FFk"); +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product9skusIsSetEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("OK+Ggpve7B0", sceNpCppWebApiUnknown59); -int APS5_VABI sceNpCppWebApiUnknown59(void) { - NotImplemented_nid_no_patch("OK+Ggpve7B0"); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData11setobjectIdEPKc(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("P3daBLFFREw", sceNpCppWebApiUnknown60); -int APS5_VABI sceNpCppWebApiUnknown60(void) { - NotImplemented_nid_no_patch("P3daBLFFREw"); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUserC1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("RZcseJ3THfY", sceNpCppWebApiUnknown61); -int APS5_VABI sceNpCppWebApiUnknown61(void) { - NotImplemented_nid_no_patch("RZcseJ3THfY"); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser10initializeEPNS1_6Common10LibContextEPKcSA_(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("RbvteD35X-8", sceNpCppWebApiUnknown62); -int APS5_VABI sceNpCppWebApiUnknown62(void) { - NotImplemented_nid_no_patch("RbvteD35X-8"); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser9setfieldsEPKc(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("XCtf+23QT0k", sceNpCppWebApiUnknown63); -int APS5_VABI sceNpCppWebApiUnknown63(void) { - NotImplemented_nid_no_patch("XCtf+23QT0k"); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser9terminateEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("a4q15LI1a4E", sceNpCppWebApiUnknown64); -int APS5_VABI sceNpCppWebApiUnknown64(void) { - NotImplemented_nid_no_patch("a4q15LI1a4E"); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUserD1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("eT4TQB7OsLA", sceNpCppWebApiUnknown65); -int APS5_VABI sceNpCppWebApiUnknown65(void) { - NotImplemented_nid_no_patch("eT4TQB7OsLA"); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi26getMultiDataStatusesByUserEiRKNS4_37ParameterToGetMultiDataStatusesByUserERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_32GetMultiDataStatusesResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("g2dEqFnhFuw", sceNpCppWebApiUnknown66); -int APS5_VABI sceNpCppWebApiUnknown66(void) { - NotImplemented_nid_no_patch("g2dEqFnhFuw"); +int APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V110DataStatus11getDataSizeEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("lPAMjFZEpt8", sceNpCppWebApiUnknown67); -int APS5_VABI sceNpCppWebApiUnknown67(void) { - NotImplemented_nid_no_patch("lPAMjFZEpt8"); +int APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V110DataStatus11getObjectIdEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("mBagn+lW-iM", sceNpCppWebApiUnknown68); -int APS5_VABI sceNpCppWebApiUnknown68(void) { - NotImplemented_nid_no_patch("mBagn+lW-iM"); +int APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V110DataStatus8getOwnerEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("mkWsKEh0h0o", sceNpCppWebApiUnknown69); -int APS5_VABI sceNpCppWebApiUnknown69(void) { - NotImplemented_nid_no_patch("mkWsKEh0h0o"); +int APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V110DataStatus9getSlotIdEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("nE0ooeCMRm8", sceNpCppWebApiUnknown70); -int APS5_VABI sceNpCppWebApiUnknown70(void) { - NotImplemented_nid_no_patch("nE0ooeCMRm8"); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V132GetMultiDataStatusesResponseBody17getDataStatusListEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("qf2I9BlKXis", sceNpCppWebApiUnknown71); -int APS5_VABI sceNpCppWebApiUnknown71(void) { - NotImplemented_nid_no_patch("qf2I9BlKXis"); +int APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V132GetMultiDataStatusesResponseBody23getTotalDataStatusCountEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("xwy1I52dE8o", sceNpCppWebApiUnknown72); -int APS5_VABI sceNpCppWebApiUnknown72(void) { - NotImplemented_nid_no_patch("xwy1I52dE8o"); +int APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V15Owner12getAccountIdEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } } diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index c40dbfd360..ba654ff02f 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -191,4 +191,24 @@ int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfoList() { return SCE_NP_ERROR_SIGNED_OUT; } +int APS5_VABI sceNpEntitlementAccessGetAddcontEntitlementInfoIndividual() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessPollConsumableEntitlementInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessRequestConsumableEntitlementInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessRequestConsumeEntitlement() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpGameIntent/Export.cpp b/core/libs/prx/libSceNpGameIntent/Export.cpp index 22fd576fae..cb36cdac0b 100644 --- a/core/libs/prx/libSceNpGameIntent/Export.cpp +++ b/core/libs/prx/libSceNpGameIntent/Export.cpp @@ -73,4 +73,54 @@ int APS5_VABI sceNpGameIntentVshTerminate(void) { return 0; } +int APS5_VABI sceNpAppLauncherInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpAppLauncherLaunchApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpAppLauncherTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGameIntentGetUdsNpComId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGameIntentLaunchApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGameIntentLaunchApp2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGameIntentNotifyAppLaunched() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionServiceGetJoinedPartySession() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionServiceGetRunningGamePlayerSession() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionServiceSendPlayerSessionInvitations() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpSessionSignaling/Export.cpp b/core/libs/prx/libSceNpSessionSignaling/Export.cpp index e75842f866..7dfa95ff73 100644 --- a/core/libs/prx/libSceNpSessionSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSessionSignaling/Export.cpp @@ -109,4 +109,39 @@ int APS5_VABI sceNpSessionSignalingGetConnectionFromNetAddress2() { return 0; } +int APS5_VABI sceNpSessionSignalingCreateJoinActivate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingGetConnectionFromNetAddress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingGetCustomProperty() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingGetGroupFromPeerAddress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingInitializeWithPort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingJoinActivate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingSetCustomProperty() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index 1157934220..df84382d86 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -204,4 +204,54 @@ int APS5_VABI sceNpWebApi2SetRequestTimeout(int64_t request_id, uint32_t timeout return 0; } +int APS5_VABI sceNpWebApi2AddMultipartPart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2CreateMultipartRequest(int user_ctx_id, const char* api_group, const char* path, const char* method, int64_t* request_id) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2InitializeForPresence(int lib_http_ctx_id, uint64_t pool_size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2IntCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2IntInitialize(const void* args) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2IntInitialize2(const void* args) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2IntPushEventCreateCtxIndFilter() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2IntPushEventCreateDestPs4CtxIndFilter() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2SendMultipartRequest(int64_t request_id, int part_index, void* data, uint64_t data_size, void* resp_info_option) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2SetMultipartContentType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceOpusCeltDec/Export.cpp b/core/libs/prx/libSceOpusCeltDec/Export.cpp index c81bf104bc..2b04cca7be 100644 --- a/core/libs/prx/libSceOpusCeltDec/Export.cpp +++ b/core/libs/prx/libSceOpusCeltDec/Export.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libSceOpusDec/include/OpusDecoder.hpp" #include +#include "prx/libc/include/General.hpp" namespace { @@ -26,4 +27,29 @@ int APS5_VABI sceOpusCeltDecDecode(void* state, const std::uint8_t* data, int by int APS5_VABI sceOpusCeltDecDestroy(void* state) { return library.Destroy(state); } +int APS5_VABI sceOpusCeltDecCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltDecCtl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltDecDecodeFloat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltDecDecodeFloatWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltDecDecodeWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceOpusCeltEnc/Export.cpp b/core/libs/prx/libSceOpusCeltEnc/Export.cpp index fb34206c21..df4e7d1854 100644 --- a/core/libs/prx/libSceOpusCeltEnc/Export.cpp +++ b/core/libs/prx/libSceOpusCeltEnc/Export.cpp @@ -5,6 +5,59 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceOpusCeltEncCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncCreateEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncCtl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncEncode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncEncodeFloat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncEncodeFloatWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncEncodeWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncGetSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceOpusDec/Export.cpp b/core/libs/prx/libSceOpusDec/Export.cpp index e9ba02c9d5..a02f1d05be 100644 --- a/core/libs/prx/libSceOpusDec/Export.cpp +++ b/core/libs/prx/libSceOpusDec/Export.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libSceOpusDec/include/OpusDecoder.hpp" #include +#include "prx/libc/include/General.hpp" namespace { @@ -26,4 +27,44 @@ int APS5_VABI sceOpusDecDecode(void* state, const std::uint8_t* data, int bytes, int APS5_VABI sceOpusDecDestroy(void* state) { return library.Destroy(state); } +int APS5_VABI sceOpusDecCancel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecCtl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecDecodeFloat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecDecodeFloatWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecDecodeFloatWithPriorityAndTimeout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecDecodeWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecDecodeWithPriorityAndTimeout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceOpusSilkEnc/Export.cpp b/core/libs/prx/libSceOpusSilkEnc/Export.cpp index d81f4a5e32..201dab2e4b 100644 --- a/core/libs/prx/libSceOpusSilkEnc/Export.cpp +++ b/core/libs/prx/libSceOpusSilkEnc/Export.cpp @@ -1,10 +1,78 @@ -#include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceOpusSilkEncCancel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncCreateEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncCtl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeFloat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeFloatWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeFloatWithPriorityAndTimeout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeWithPriorityAndTimeout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncGetSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 2bd8a89989..68b159393a 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -177,13 +177,13 @@ int APS5_VABI scePadOutputReport() { NotImplemented_nid_no_patch(__func__); retu int APS5_VABI scePadRead_nid_postfix(int handle, PadData* data, int num) { if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; - if (data == nullptr || num <= 0) APS5_INVALID_ARG_EX; + if (data == nullptr || num <= 0) return PAD_ERROR_INVALID_ARG; return Pad::Read(data, num); } int APS5_VABI scePadReadState(int handle, PadData* data) { if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; - if (data == nullptr) APS5_INVALID_ARG_EX; + if (data == nullptr) return PAD_ERROR_INVALID_ARG; *data = Pad::ReadState(); diff --git a/core/libs/prx/libScePad/include/InputMapping.hpp b/core/libs/prx/libScePad/include/InputMapping.hpp index fe2d7a0339..ec4da4ab47 100644 --- a/core/libs/prx/libScePad/include/InputMapping.hpp +++ b/core/libs/prx/libScePad/include/InputMapping.hpp @@ -39,6 +39,7 @@ struct InputConfiguration { inline constexpr std::array InputMapping{ InputBinding{SDL_SCANCODE_F11, MouseButton::None, InputControl::ToggleFullscreen}, InputBinding{SDL_SCANCODE_F10, MouseButton::None, InputControl::TogglePerformanceOverlay}, + InputBinding{SDL_SCANCODE_F12, MouseButton::None, InputControl::Screenshot}, InputBinding{SDL_SCANCODE_RETURN, MouseButton::None, InputControl::Button, PadButton::Cross}, InputBinding{SDL_SCANCODE_SPACE, MouseButton::None, InputControl::Button, PadButton::Cross}, InputBinding{SDL_SCANCODE_ESCAPE, MouseButton::None, InputControl::Button, PadButton::Options}, diff --git a/core/libs/prx/libScePad/include/KeyboardMouse.hpp b/core/libs/prx/libScePad/include/KeyboardMouse.hpp index dbe86e49e1..14002488e1 100644 --- a/core/libs/prx/libScePad/include/KeyboardMouse.hpp +++ b/core/libs/prx/libScePad/include/KeyboardMouse.hpp @@ -54,6 +54,7 @@ private: struct InputRequests { bool toggleFullscreen = false; bool togglePerformanceOverlay = false; + bool screenshot = false; bool toggleMouse = false; bool releaseMouse = false; }; diff --git a/core/libs/prx/libScePad/include/PadInputTypes.hpp b/core/libs/prx/libScePad/include/PadInputTypes.hpp index 891d3eb710..f8e544e092 100644 --- a/core/libs/prx/libScePad/include/PadInputTypes.hpp +++ b/core/libs/prx/libScePad/include/PadInputTypes.hpp @@ -23,7 +23,8 @@ enum class InputControl { Shake, ToggleMouse, ToggleFullscreen, - TogglePerformanceOverlay + TogglePerformanceOverlay, + Screenshot }; enum class PadButton : std::uint32_t { diff --git a/core/libs/prx/libScePlayGo/Export.cpp b/core/libs/prx/libScePlayGo/Export.cpp index 2bc5bfc67c..555a68f373 100644 --- a/core/libs/prx/libScePlayGo/Export.cpp +++ b/core/libs/prx/libScePlayGo/Export.cpp @@ -22,6 +22,7 @@ static constexpr int8_t PLAYGO_LOCUS_LOCAL_FAST = 3; static constexpr uint64_t PLAYGO_LANGUAGE_MASK_ALL = ~0ull; static constexpr uint64_t PLAYGO_OPTIONAL_CHUNK_MASK_ALL = ~0ull; static constexpr int32_t PLAYGO_INSTALL_SPEED_FULL = 2; +static constexpr uint32_t PLAYGO_SCENARIO_CHUNK_COUNT = 1000; static int32_t g_installSpeed = PLAYGO_INSTALL_SPEED_FULL; @@ -68,6 +69,7 @@ static const ChunkTable& Chunks() { if (ReadPackageChunks(table)) return table; table.scenario = std::ifstream(ResolvePath_nid_no_patch("/app0/sce_sys/playgo-scenario.json")).good(); if (!table.scenario) RejectUnreadablePlayGoFiles(); + else for (uint32_t id = 1; id < PLAYGO_SCENARIO_CHUNK_COUNT; ++id) table.ids.insert(static_cast(id)); return table; } table.defined = true; @@ -260,4 +262,19 @@ int APS5_VABI scePlayGoTerminate(void) { return 0; } +int APS5_VABI scePlayGoRequestNextChunk() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayGoSetLanguageMask(std::uint32_t handle, std::uint64_t languageMask) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayGoSnapshot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libScePngDec/Export.cpp b/core/libs/prx/libScePngDec/Export.cpp index c4fa2c0bcd..419c7966f0 100644 --- a/core/libs/prx/libScePngDec/Export.cpp +++ b/core/libs/prx/libScePngDec/Export.cpp @@ -8,6 +8,7 @@ #include #include "Decoder/Png.hpp" #include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" static_assert(sizeof(PngDecCreateParam) == 0xC); static_assert(sizeof(PngDecParseParam) == 0x10); @@ -194,4 +195,9 @@ int32_t APS5_VABI scePngDecQueryMemorySize(const PngDecCreateParam* param) { return static_cast(MEMORY_SIZE); } +int APS5_VABI scePngDecDecodeWithInputControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 629ee77fa5..ac7d23950f 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -31,6 +32,15 @@ constexpr std::uint64_t UNIX_EPOCH_TICK = UNIX_EPOCH_DAYS * TICKS_PER_DAY; constexpr std::uint64_t WIN32_EPOCH_TICK = WIN32_EPOCH_DAYS * TICKS_PER_DAY; constexpr std::uint64_t MAX_TICK = 3652059ull * TICKS_PER_DAY - 1; +struct ClockState { + std::uint64_t tick = 0; + std::chrono::steady_clock::time_point setAt{}; + bool isSet = false; +}; + +std::mutex clockMutex; +ClockState clocks[5]; + bool isLeapYear(std::int64_t year) { return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; } @@ -90,11 +100,31 @@ RtcDateTime fromTick(std::uint64_t tick) { static_cast(timeOfDay % TICKS_PER_MINUTE / TICKS_PER_SECOND), static_cast(timeOfDay % TICKS_PER_SECOND)}; } -std::uint64_t currentTick() { +std::uint64_t currentTick(std::size_t clock = 0) { + ClockState state; + { + std::lock_guard lock(clockMutex); + state = clocks[clock]; + } + if (state.isSet) { + const auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - state.setAt).count(); + const auto elapsedTicks = static_cast(elapsed); + return elapsedTicks > std::numeric_limits::max() - state.tick + ? std::numeric_limits::max() : state.tick + elapsedTicks; + } + if (clock == 2 || clock == 3) return currentTick(1); const auto now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); return UNIX_EPOCH_TICK + static_cast(now); } +int setCurrentTick(std::size_t clock, const RtcTick* tick) { + if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; + if (tick->tick < UNIX_EPOCH_TICK) return SCE_RTC_ERROR_INVALID_VALUE; + std::lock_guard lock(clockMutex); + clocks[clock] = ClockState{tick->tick, std::chrono::steady_clock::now(), true}; + return 0; +} + std::int64_t localOffsetSeconds(std::uint64_t utcTick) { const auto elapsed = std::chrono::microseconds{static_cast(utcTick) - static_cast(UNIX_EPOCH_TICK)}; const auto seconds = static_cast(std::chrono::floor(elapsed).count()); @@ -126,16 +156,22 @@ int addTicks(RtcTick* dst, const RtcTick* src, std::int64_t count, std::int64_t return 0; } -int addMonths(RtcTick* dst, const RtcTick* src, std::int64_t months) { +int addTicksWrapping(RtcTick* dst, const RtcTick* src, std::int64_t count, std::int64_t unit) { + if (!dst || !src) return SCE_RTC_ERROR_INVALID_POINTER; + dst->tick = src->tick + static_cast(count) * static_cast(unit); + return 0; +} + +int addMonths(RtcTick* dst, const RtcTick* src, std::int32_t months) { if (!dst || !src) return SCE_RTC_ERROR_INVALID_POINTER; - if (src->tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; RtcDateTime time = fromTick(src->tick); - const std::int64_t monthIndex = time.year * 12 + (time.month - 1) + months; - const std::int64_t year = monthIndex / 12; - if (monthIndex < 12 || year > 9999) return SCE_RTC_ERROR_INVALID_VALUE; - time.year = static_cast(year); - time.month = static_cast(monthIndex % 12 + 1); - const int lastDay = daysInMonth(time.year, time.month); + const auto index = static_cast(static_cast(time.year) * 12u + time.month + static_cast(months) - 1u); + const auto year = static_cast(index / 12); + const std::int32_t month = index % 12; + if (year < 1 || year > 9999 || month < 0) return 0; + time.year = year; + time.month = static_cast(month + 1); + const int lastDay = daysInMonth(year, time.month); if (time.day > lastDay) time.day = static_cast(lastDay); dst->tick = toTick(time); return 0; @@ -359,7 +395,9 @@ int APS5_VABI sceRtcGetCurrentTick(RtcTick* tick) { } int APS5_VABI sceRtcGetCurrentNetworkTick(RtcTick* tick) { - return sceRtcGetCurrentTick(tick); + if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; + tick->tick = currentTick(1); + return 0; } int APS5_VABI sceRtcGetCurrentClock(RtcDateTime* time, int time_zone_minutes) { @@ -595,31 +633,31 @@ int APS5_VABI sceRtcParseDateTime(RtcTick* utc, const char* date_time) { } int APS5_VABI sceRtcTickAddTicks(RtcTick* dst, const RtcTick* src, int64_t ticks) { - return addTicks(dst, src, ticks, 1); + return addTicksWrapping(dst, src, ticks, 1); } int APS5_VABI sceRtcTickAddMicroseconds(RtcTick* dst, const RtcTick* src, int64_t usec) { - return addTicks(dst, src, usec, 1); + return addTicksWrapping(dst, src, usec, 1); } int APS5_VABI sceRtcTickAddSeconds(RtcTick* dst, const RtcTick* src, int64_t seconds) { - return addTicks(dst, src, seconds, TICKS_PER_SECOND); + return addTicksWrapping(dst, src, seconds, TICKS_PER_SECOND); } int APS5_VABI sceRtcTickAddMinutes(RtcTick* dst, const RtcTick* src, int64_t minutes) { - return addTicks(dst, src, minutes, TICKS_PER_MINUTE); + return addTicksWrapping(dst, src, minutes, TICKS_PER_MINUTE); } int APS5_VABI sceRtcTickAddHours(RtcTick* dst, const RtcTick* src, int32_t hours) { - return addTicks(dst, src, hours, TICKS_PER_HOUR); + return addTicksWrapping(dst, src, hours, TICKS_PER_HOUR); } int APS5_VABI sceRtcTickAddDays(RtcTick* dst, const RtcTick* src, int32_t days) { - return addTicks(dst, src, days, TICKS_PER_DAY); + return addTicksWrapping(dst, src, days, TICKS_PER_DAY); } int APS5_VABI sceRtcTickAddWeeks(RtcTick* dst, const RtcTick* src, int32_t weeks) { - return addTicks(dst, src, weeks, 7 * TICKS_PER_DAY); + return addTicksWrapping(dst, src, weeks, 7 * TICKS_PER_DAY); } int APS5_VABI sceRtcTickAddMonths(RtcTick* dst, const RtcTick* src, int32_t months) { @@ -627,7 +665,7 @@ int APS5_VABI sceRtcTickAddMonths(RtcTick* dst, const RtcTick* src, int32_t mont } int APS5_VABI sceRtcTickAddYears(RtcTick* dst, const RtcTick* src, int16_t years) { - return addMonths(dst, src, static_cast(years) * 12); + return addMonths(dst, src, static_cast(years) * 12); } int APS5_VABI sceRtcInit(void) { @@ -645,44 +683,37 @@ int APS5_VABI sceRtcCompareTick(const RtcTick* first, const RtcTick* second) { } int APS5_VABI sceRtcGetCurrentRawNetworkTick(RtcTick* tick) { - return sceRtcGetCurrentTick(tick); + if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; + tick->tick = currentTick(4); + return 0; } int APS5_VABI sceRtcGetCurrentAdNetworkTick(RtcTick* tick) { - return sceRtcGetCurrentTick(tick); + if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; + tick->tick = currentTick(2); + return 0; } int APS5_VABI sceRtcGetCurrentDebugNetworkTick(RtcTick* tick) { - return sceRtcGetCurrentTick(tick); -} - -static int checkSettableTick(const RtcTick* tick) { if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; - return tick->tick < UNIX_EPOCH_TICK ? SCE_RTC_ERROR_INVALID_VALUE : 0; + tick->tick = currentTick(3); + return 0; } int APS5_VABI sceRtcSetCurrentTick(const RtcTick* tick) { - if (const int result = checkSettableTick(tick); result != 0) return result; - NotImplemented_nid_no_patch(__func__); - return 0; + return setCurrentTick(0, tick); } int APS5_VABI sceRtcSetCurrentNetworkTick(const RtcTick* tick) { - if (const int result = checkSettableTick(tick); result != 0) return result; - NotImplemented_nid_no_patch(__func__); - return 0; + return setCurrentTick(1, tick); } int APS5_VABI sceRtcSetCurrentAdNetworkTick(const RtcTick* tick) { - if (const int result = checkSettableTick(tick); result != 0) return result; - NotImplemented_nid_no_patch(__func__); - return 0; + return setCurrentTick(2, tick); } int APS5_VABI sceRtcSetCurrentDebugNetworkTick(const RtcTick* tick) { - if (const int result = checkSettableTick(tick); result != 0) return result; - NotImplemented_nid_no_patch(__func__); - return 0; + return setCurrentTick(3, tick); } int APS5_VABI sceRtcSetConf(int first, int second, int third, int fourth) { diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index a4861da3bd..87864f5971 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -1,10 +1,14 @@ #include +#include +#include #include #include #include #include +#include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -20,6 +24,13 @@ constexpr int RUDP_ERROR_MEMORY = static_cast(0x80770007u); constexpr int RUDP_ERROR_CONN_RESET = static_cast(0x80770009u); constexpr int RUDP_ERROR_CONN_REFUSED = static_cast(0x8077000Au); constexpr int RUDP_ERROR_NOT_ACCEPTABLE = static_cast(0x80770011u); +constexpr int RUDP_ERROR_INVALID_POLL_ID = static_cast(0x8077001Fu); +constexpr int RUDP_ERROR_TOO_MANY_CONTEXTS = static_cast(0x80770020u); + +constexpr int RUDP_POLL_OP_ADD = 1; +constexpr int RUDP_POLL_OP_MODIFY = 2; +constexpr int RUDP_POLL_OP_REMOVE = 3; +constexpr uint64_t RUDP_POLL_WAIT_MAX_USEC = static_cast(std::numeric_limits::max() / 2000); constexpr int RUDP_POOL_MIN_SIZE = 0xC58; constexpr std::uintptr_t RUDP_POOL_ALIGNMENT = 8; @@ -36,9 +47,31 @@ std::map g_ctx; // id -> bound GuestRudpEventHandler g_handler = nullptr; void* g_handler_arg = nullptr; +struct RudpPoll { + uint32_t capacity; + std::map entries; +}; + +struct RudpPollEvent { + int ctxId; + uint16_t reqEvents; + uint16_t rtnEvents; +}; +static_assert(sizeof(RudpPollEvent) == 8); + +int g_next_poll = 1; +std::map g_polls; +std::condition_variable g_polls_changed; + bool valid_ctx(int ctx) { return g_ctx.count(ctx) != 0; } + +void remove_ctx_from_polls(int ctx) { + for (auto& [id, poll] : g_polls) { + poll.entries.erase(ctx); + } +} } // namespace extern "C" { @@ -84,6 +117,8 @@ int APS5_VABI sceRudpEnd(void) { } g_inited = false; g_ctx.clear(); + g_polls.clear(); + g_polls_changed.notify_all(); return 0; } @@ -92,7 +127,11 @@ int APS5_VABI sceRudpTerminate(int ctx_id) { if (!g_inited) { return RUDP_ERROR_NOT_INITIALIZED; } - return g_ctx.erase(ctx_id) != 0 ? 0 : RUDP_ERROR_INVALID_CONTEXT_ID; + if (g_ctx.erase(ctx_id) == 0) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + remove_ctx_from_polls(ctx_id); + return 0; } int APS5_VABI sceRudpEnableInternalIOThread(uint32_t stack_size, uint32_t priority) { @@ -293,22 +332,121 @@ int APS5_VABI sceRudpGetMaxSegmentSize(uint16_t* size) { return 0; } -int APS5_VABI sceRudpPollCreate() { +int APS5_VABI sceRudpPollCreate(uint32_t size) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (size == 0) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + if (g_next_poll == std::numeric_limits::max()) { + throw std::runtime_error("sceRudpPollCreate: poll ids exhausted"); + } + const int id = g_next_poll++; + g_polls[id] = RudpPoll{size, {}}; + return id; +} + +int APS5_VABI sceRudpPollControl(int pollId, int op, int ctxId, uint16_t events) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (pollId < 0 || ctxId < 0 || (op != RUDP_POLL_OP_ADD && op != RUDP_POLL_OP_MODIFY && op != RUDP_POLL_OP_REMOVE)) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + const auto poll = g_polls.find(pollId); + if (poll == g_polls.end()) { + return RUDP_ERROR_INVALID_POLL_ID; + } + if (!valid_ctx(ctxId)) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + auto& entries = poll->second.entries; + const auto entry = entries.find(ctxId); + if (op == RUDP_POLL_OP_ADD) { + if (entries.size() >= poll->second.capacity) { + return RUDP_ERROR_TOO_MANY_CONTEXTS; + } + if (entry != entries.end()) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + entries.emplace(ctxId, events); + return 0; + } + if (entry == entries.end()) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + if (op == RUDP_POLL_OP_MODIFY) { + entry->second = events; + } else { + entries.erase(entry); + } + return 0; +} + +int APS5_VABI sceRudpPollWait(int pollId, RudpPollEvent* events, uint32_t eventLen, uint64_t timeout) { + std::unique_lock lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (pollId < 0 || events == nullptr || eventLen == 0) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + if (g_polls.count(pollId) == 0) { + return RUDP_ERROR_INVALID_POLL_ID; + } + const auto closed = [pollId] { return !g_inited || g_polls.count(pollId) == 0; }; + if (timeout > RUDP_POLL_WAIT_MAX_USEC) { + g_polls_changed.wait(lk, closed); + } else if (!g_polls_changed.wait_for(lk, std::chrono::microseconds(timeout), closed)) { + return 0; + } + return g_inited ? RUDP_ERROR_INVALID_POLL_ID : RUDP_ERROR_NOT_INITIALIZED; +} + +int APS5_VABI sceRudpPollDestroy(int pollId) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (pollId < 0) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + if (g_polls.erase(pollId) == 0) { + return RUDP_ERROR_INVALID_POLL_ID; + } + g_polls_changed.notify_all(); + return 0; +} + +int APS5_VABI sceRudpAccept() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceRudpPollControl() { +int APS5_VABI sceRudpFlush() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceRudpPollWait() { +int APS5_VABI sceRudpGetLocalInfo() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceRudpPollDestroy() { +int APS5_VABI sceRudpListen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpPollCancel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpSetMaxSegmentSize() { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceS3da/Export.cpp b/core/libs/prx/libSceS3da/Export.cpp index d81f4a5e32..34b8330de5 100644 --- a/core/libs/prx/libSceS3da/Export.cpp +++ b/core/libs/prx/libSceS3da/Export.cpp @@ -1,10 +1,28 @@ -#include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceS3daClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceS3daGetInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceS3daOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceS3daSendData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index b5d52e5013..5bb5858274 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -690,7 +690,7 @@ int APS5_VABI sceSaveDataSaveIconByPath(const SaveDataMountPoint* mount_point, c static int setParam(const SaveDataMountPoint* mount_point, uint32_t param_type, const void* param_buf, size_t param_buf_size) { if (mount_point == nullptr || param_buf == nullptr) { - throw std::runtime_error("sceSaveDataSetParam: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); const int slot = find_slot_by_mount_point(mount_point->data); @@ -994,4 +994,49 @@ int APS5_VABI sceSaveDataPrepareForTransferring() { return 0; } +int APS5_VABI sceSaveDataCheckIpmiIfSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDebug() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDebugCheckBackupData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDebugCleanMount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDebugCompiledSdkVersion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDebugCreateSaveDataRoot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDebugProspero() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDebugRemoveSaveDataRoot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDebugTarget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceSaveData/SaveDataFile.hpp b/core/libs/prx/libSceSaveData/SaveDataFile.hpp index 193c1ffbb2..84fd888180 100644 --- a/core/libs/prx/libSceSaveData/SaveDataFile.hpp +++ b/core/libs/prx/libSceSaveData/SaveDataFile.hpp @@ -1,8 +1,11 @@ #pragma once +#include #include +#include #include -#include +#include +#include #include #ifdef _WIN32 @@ -20,6 +23,7 @@ namespace savedata { template inline bool replace_file_with(const std::filesystem::path& path, const void* data, std::size_t size, ReplaceOperation&& replace_operation) { + if (size != 0 && data == nullptr) return false; const auto parent = path.parent_path(); std::error_code error; if (!parent.empty()) { @@ -29,44 +33,39 @@ inline bool replace_file_with(const std::filesystem::path& path, const void* dat } } - auto temporary = path; - temporary += ".tmp"; - const auto remove_temporary = [&temporary]() { - std::error_code ignored; - std::filesystem::remove(temporary, ignored); - }; - - { - std::ofstream file(temporary, std::ios::binary | std::ios::trunc); - if (!file) { - remove_temporary(); - return false; - } - if (size != 0) { - if (data == nullptr) { - file.close(); - remove_temporary(); - return false; - } - file.write(static_cast(data), static_cast(size)); - } - file.flush(); - if (!file) { - file.close(); - remove_temporary(); - return false; - } - file.close(); - if (!file) { - remove_temporary(); - return false; - } + std::filesystem::path temporary; + std::FILE* file = nullptr; + std::random_device random; + for (unsigned attempt = 0; attempt < 128; ++attempt) { + temporary = path; + temporary += ".tmp." + std::to_string(random()) + "." + std::to_string(random()); +#ifdef _WIN32 + file = _wfopen(temporary.c_str(), L"wbx"); +#else + file = std::fopen(temporary.c_str(), "wbx"); +#endif + if (file) break; + if (errno != EEXIST) return false; } + if (!file) return false; + struct Cleanup { + const std::filesystem::path& path; + std::FILE*& file; + bool remove = true; + ~Cleanup() { + if (file) std::fclose(file); + std::error_code ignored; + if (remove) std::filesystem::remove(path, ignored); + } + } cleanup{temporary, file}; + + const bool written = size == 0 || std::fwrite(data, 1, size, file) == size; + const bool closed = std::fclose(file) == 0; + file = nullptr; + if (!written || !closed) return false; const bool replaced = replace_operation(temporary, path); - if (!replaced) { - remove_temporary(); - } + cleanup.remove = !replaced; return replaced; } diff --git a/core/libs/prx/libSceScreenShot/Export.cpp b/core/libs/prx/libSceScreenShot/Export.cpp index 41e538d9cc..ae99fefcf6 100644 --- a/core/libs/prx/libSceScreenShot/Export.cpp +++ b/core/libs/prx/libSceScreenShot/Export.cpp @@ -13,5 +13,49 @@ int APS5_VABI sceScreenShotSetOverlayImageWithOrigin() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceScreenShotCapture() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotDisable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotDisableNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotEnable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotEnableNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotGetAppInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotGetDrcParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotIsDisabled() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotIsVshScreenCaptureDisabled() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotSetDrcParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotSetParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceShellUIUtil/Export.cpp b/core/libs/prx/libSceShellUIUtil/Export.cpp index d81f4a5e32..4ff433cad7 100644 --- a/core/libs/prx/libSceShellUIUtil/Export.cpp +++ b/core/libs/prx/libSceShellUIUtil/Export.cpp @@ -1,10 +1,38 @@ -#include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceShellUIUtilCreateTaskToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilGetAppUrl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilLaunchByUri() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilLaunchTask() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 5d7394fdb3..636719b717 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -1,7 +1,6 @@ #ifdef _WIN32 #include #else -#include #include #include #endif @@ -52,15 +51,21 @@ constexpr int ErrorFatalAlert = static_cast(0x8095f010); constexpr int ErrorWantPollIn = static_cast(0x8095f013); constexpr int ErrorWantPollOut = static_cast(0x8095f014); constexpr int ErrorSocketClosed = static_cast(0x8095170d); -constexpr int ErrorReadTimeout = static_cast(0x80951710); constexpr int ErrorCallbackCheck = static_cast(0x8095f0fd); -constexpr int ReadPartial = 0x1; +constexpr int ErrorInvalidValue = static_cast(0x8095f007); +constexpr int ErrorBadLength = static_cast(0x80951773); +constexpr int ErrorBadRecord = static_cast(0x80951d55); +constexpr int ErrorNetBadDescriptor = static_cast(0x80410109); +constexpr int ErrorNetPipe = static_cast(0x80410120); +constexpr int ErrorNetReset = static_cast(0x80410136); +constexpr int ErrorKernelInvalid = static_cast(0x80020016); constexpr std::uint32_t FlagServerVerify = 0x1; constexpr std::uint32_t FlagCnCheck = 0x4; constexpr std::uint32_t FlagNotAfterCheck = 0x8; constexpr std::uint32_t FlagNotBeforeCheck = 0x10; constexpr std::uint32_t FlagKnownCaCheck = 0x20; -constexpr std::uint32_t InitialVerifyOptions = FlagServerVerify | FlagCnCheck | FlagNotAfterCheck | FlagNotBeforeCheck | FlagKnownCaCheck; +constexpr std::uint32_t VerifyOptionMask = 0x3f; +constexpr std::uint32_t InitialVerifyOptions = FlagServerVerify | FlagCnCheck | FlagKnownCaCheck; constexpr std::int64_t TicksPerSecond = 1000000; constexpr std::int64_t TicksPerMinute = 60 * TicksPerSecond; @@ -263,18 +268,19 @@ bool HostReset(int error) { #endif } -bool HostTimedOut(std::uintptr_t socket, int error) { +bool HostTimedOut(int error) { #ifdef _WIN32 - (void)socket; return error == WSAETIMEDOUT; #else - if (!HostWouldBlock(error)) return false; - const int flags = ::fcntl(static_cast(socket), F_GETFL); - return flags >= 0 && !(flags & O_NONBLOCK); + (void)error; + return false; #endif } +enum class Operation { Connect, Read, Write, Close }; + struct Session { + int contextHandle; std::shared_ptr context; std::shared_ptr identity; std::shared_ptr peer; @@ -291,15 +297,17 @@ struct Session { bool verifying = false; bool verified = false; bool closed = false; - bool timedOut = false; + bool reset = false; + bool gone = false; int verifyResult = 0; + unsigned int failedOptions = 0; Random random; mbedtls_x509_crt authorities; mbedtls_ssl_config config; mbedtls_ssl_context ssl; - Session(std::shared_ptr owner, std::shared_ptr socketHolder, std::uintptr_t native) - : context(std::move(owner)), identity(context->identity), socketOwner(std::move(socketHolder)), socket(native) { + Session(int handle, std::shared_ptr owner, std::shared_ptr socketHolder, std::uintptr_t native) + : contextHandle(handle), context(std::move(owner)), identity(context->identity), socketOwner(std::move(socketHolder)), socket(native) { mbedtls_x509_crt_init(&authorities); mbedtls_ssl_config_init(&config); mbedtls_ssl_init(&ssl); @@ -346,7 +354,6 @@ struct Session { static int Send(void* argument, const unsigned char* buffer, std::size_t length) { auto& session = *static_cast(argument); const auto chunk = static_cast(std::min(length, INT_MAX)); - session.timedOut = false; for (;;) { #ifdef _WIN32 const auto sent = ::send(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); @@ -356,8 +363,7 @@ struct Session { if (sent >= 0) return static_cast(sent); const int error = HostError(); if (HostInterrupted(error)) continue; - session.timedOut = HostTimedOut(session.socket, error); - if (session.timedOut || HostWouldBlock(error)) return MBEDTLS_ERR_SSL_WANT_WRITE; + if (HostWouldBlock(error) || HostTimedOut(error)) return MBEDTLS_ERR_SSL_WANT_WRITE; if (HostReset(error)) return MBEDTLS_ERR_NET_CONN_RESET; session.hostError = error; return MBEDTLS_ERR_NET_SEND_FAILED; @@ -367,7 +373,6 @@ struct Session { static int Receive(void* argument, unsigned char* buffer, std::size_t length) { auto& session = *static_cast(argument); const auto chunk = static_cast(std::min(length, INT_MAX)); - session.timedOut = false; for (;;) { #ifdef _WIN32 const auto received = ::recv(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); @@ -377,41 +382,49 @@ struct Session { if (received >= 0) return static_cast(received); const int error = HostError(); if (HostInterrupted(error)) continue; - session.timedOut = HostTimedOut(session.socket, error); - if (session.timedOut || HostWouldBlock(error)) return MBEDTLS_ERR_SSL_WANT_READ; + if (HostWouldBlock(error) || HostTimedOut(error)) return MBEDTLS_ERR_SSL_WANT_READ; if (HostReset(error)) return MBEDTLS_ERR_NET_CONN_RESET; session.hostError = error; return MBEDTLS_ERR_NET_RECV_FAILED; } } - int VerifyCode() const { + void Verify() { const auto flags = mbedtls_ssl_get_verify_result(&ssl); - if (!(verifyOptions & FlagServerVerify)) return 0; - if ((verifyOptions & FlagKnownCaCheck) && (flags & MBEDTLS_X509_BADCERT_NOT_TRUSTED)) return ErrorUnknownCa; - if ((verifyOptions & FlagCnCheck) && (flags & MBEDTLS_X509_BADCERT_CN_MISMATCH)) return ErrorCnCheck; - if ((verifyOptions & FlagNotAfterCheck) && (flags & MBEDTLS_X509_BADCERT_EXPIRED)) return ErrorNotAfterCheck; - if ((verifyOptions & FlagNotBeforeCheck) && (flags & MBEDTLS_X509_BADCERT_FUTURE)) return ErrorNotBeforeCheck; + failedOptions = 0; + verifyResult = 0; + if (!(verifyOptions & FlagServerVerify)) return; + if ((verifyOptions & FlagKnownCaCheck) && (flags & MBEDTLS_X509_BADCERT_NOT_TRUSTED)) failedOptions |= FlagKnownCaCheck; + if ((verifyOptions & FlagCnCheck) && (flags & MBEDTLS_X509_BADCERT_CN_MISMATCH)) failedOptions |= FlagCnCheck; + if ((verifyOptions & FlagNotAfterCheck) && (flags & MBEDTLS_X509_BADCERT_EXPIRED)) failedOptions |= FlagNotAfterCheck; + if ((verifyOptions & FlagNotBeforeCheck) && (flags & MBEDTLS_X509_BADCERT_FUTURE)) failedOptions |= FlagNotBeforeCheck; constexpr std::uint32_t optional = MBEDTLS_X509_BADCERT_NOT_TRUSTED | MBEDTLS_X509_BADCERT_CN_MISMATCH | MBEDTLS_X509_BADCERT_EXPIRED | MBEDTLS_X509_BADCERT_FUTURE; - return (flags & ~optional) ? ErrorInvalidCert : 0; + if (failedOptions & FlagKnownCaCheck) verifyResult = ErrorUnknownCa; + else if (failedOptions & FlagCnCheck) verifyResult = ErrorCnCheck; + else if (failedOptions & FlagNotAfterCheck) verifyResult = ErrorNotAfterCheck; + else if (failedOptions & FlagNotBeforeCheck) verifyResult = ErrorNotBeforeCheck; + else if (flags & ~optional) verifyResult = ErrorInvalidCert; } - int Code(int result) const { + int Code(int result, Operation operation) { switch (result) { - case MBEDTLS_ERR_SSL_WANT_READ: return timedOut ? ErrorReadTimeout : ErrorWantPollIn; - case MBEDTLS_ERR_SSL_WANT_WRITE: return timedOut ? 0 : ErrorWantPollOut; - case MBEDTLS_ERR_SSL_CONN_EOF: - case MBEDTLS_ERR_NET_CONN_RESET: return ErrorSocketClosed; + case MBEDTLS_ERR_SSL_WANT_READ: return ErrorWantPollIn; + case MBEDTLS_ERR_SSL_WANT_WRITE: return ErrorWantPollOut; + case MBEDTLS_ERR_NET_CONN_RESET: + reset = true; + if (operation == Operation::Write) return ErrorNetPipe; + if (operation == Operation::Connect) return ErrorSocketClosed; + return ErrorNetReset; + case MBEDTLS_ERR_SSL_CONN_EOF: return operation == Operation::Connect ? ErrorSocketClosed : 0; case MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE: return ErrorFatalAlert; + case MBEDTLS_ERR_SSL_INVALID_MAC: return operation == Operation::Read ? ErrorBadRecord : 0; default: return 0; } } [[noreturn]] void Fail(const char* function, int error) const { if (currentPoolExhausted || error == MBEDTLS_ERR_SSL_ALLOC_FAILED) throw std::bad_alloc(); - if (error == MBEDTLS_ERR_SSL_WANT_WRITE && timedOut) - throw std::runtime_error(std::string(function) + ": the socket's send timeout expired; the console's error code is unknown"); char text[160]; mbedtls_strerror(error, text, sizeof(text)); auto message = std::string(function) + ": " + text; @@ -426,7 +439,7 @@ struct Session { using SslVerifyCallback = int (APS5_VABI *)(int, unsigned int, void**, int, void*); -constexpr int SslVersionNone = 0; +constexpr int SslVersionSsl30 = 2; constexpr int SslVersionTls12 = 5; struct SslConnection { @@ -436,7 +449,7 @@ struct SslConnection { SslVerifyCallback verifyCallback; void* verifyArgument; std::uint32_t verifyOptions = InitialVerifyOptions; - int minimumVersion = SslVersionNone; + int minimumVersion = SslVersionTls12; std::string alpn; std::shared_ptr session; }; @@ -463,26 +476,22 @@ Context& GetContext(int handle) { return *found->second; } -SslConnection& RequireConnection(const char* function, int sslConnectionId) { +SslConnection* FindConnection(int sslConnectionId) { const auto connection = State().connections.find(sslConnectionId); - if (connection == State().connections.end()) throw std::invalid_argument(std::string(function) + ": unknown connection"); - if (!State().contexts.contains(connection->second.context)) { - throw std::invalid_argument(std::string(function) + ": the connection's context was terminated"); - } - return connection->second; + if (connection == State().connections.end()) return nullptr; + if (!State().contexts.contains(connection->second.context)) return nullptr; + return &connection->second; } -std::shared_ptr RequireSession(const char* function, int sslConnectionId) { - std::lock_guard lock(State().mutex); - auto& connection = RequireConnection(function, sslConnectionId); - if (!connection.session) throw std::logic_error(std::string(function) + ": the connection is not connected"); - return connection.session; +SslConnection& RequireConnection(const char* function, int sslConnectionId) { + auto* connection = FindConnection(sslConnectionId); + if (!connection) throw std::invalid_argument(std::string(function) + ": unknown connection"); + return *connection; } -void RequireVerified(const char* function, const Session& session) { - if (!session.verified) throw std::logic_error(std::string(function) + ": the connection is not connected"); - if (session.verifyResult != 0) throw std::logic_error(std::string(function) + ": the connection failed verification"); - if (session.closed) throw std::logic_error(std::string(function) + ": the connection was closed"); +void RequireUsable(const char* function, const Session& session) { + if (session.verifyResult != 0 && session.verifyResult != ErrorCallbackCheck) + throw std::logic_error(std::string(function) + ": the connection failed verification; the console's behaviour is unknown"); } void RegisterPeer(Session& session) { @@ -554,6 +563,17 @@ const mbedtls_x509_crt* RequireCertificate(const char* function, int sslCtxId, v throw std::invalid_argument(std::string(function) + ": unknown certificate"); } +const char* LongAttributeName(const char* shortName) { + static constexpr std::pair names[] = { + {"CN", "commonName"}, {"C", "countryName"}, {"L", "localityName"}, {"ST", "stateOrProvinceName"}, + {"O", "organizationName"}, {"OU", "organizationalUnitName"}, {"SN", "surname"}, {"GN", "givenName"}, + {"DC", "domainComponent"}, {"uid", "userId"}, + }; + for (const auto& [name, longName] : names) + if (std::strcmp(shortName, name) == 0) return longName; + return shortName; +} + void* ExtractCertName(const char* function, int sslCtxId, void* sslCert, bool subject) { std::lock_guard lock(State().mutex); auto& context = GetContext(sslCtxId); @@ -565,7 +585,7 @@ void* ExtractCertName(const char* function, int sslCtxId, void* sslCert, bool su std::string oid; const char* shortName = nullptr; if (mbedtls_oid_get_attr_short_name(&node->oid, &shortName) == 0 && shortName) { - oid = shortName; + oid = LongAttributeName(shortName); } else { char numeric[64] = {}; if (mbedtls_oid_get_numeric_string(numeric, sizeof(numeric), &node->oid) > 0) { @@ -588,6 +608,7 @@ void* ExtractCertName(const char* function, int sslCtxId, void* sslCert, bool su extern "C" { int APS5_VABI sceSslInit_nid_postfix(std::uint64_t poolSize) { + if (poolSize == 0) return ErrorKernelInvalid; auto& state = State(); std::lock_guard lock(state.mutex); if (state.nextHandle > static_cast(std::numeric_limits::max())) throw std::overflow_error("SSL context handles exhausted"); @@ -600,18 +621,18 @@ int APS5_VABI sceSslInit_nid_postfix(std::uint64_t poolSize) { int APS5_VABI sceSslTerm_nid_postfix(int sslCtxId) { auto& state = State(); std::lock_guard lock(state.mutex); - state.contexts.erase(sslCtxId); + if (!state.contexts.erase(sslCtxId)) return ErrorInvalidId; return 0; } int APS5_VABI sceSslGetMemoryPoolStats(int sslCtxId, SceSsl::MemoryPoolStats* stats) { - if (!stats) return static_cast(0x8095F007); + if (!stats) return ErrorInvalidValue; auto& state = State(); std::lock_guard lock(state.mutex); const auto found = state.contexts.find(sslCtxId); if (found == state.contexts.end()) { std::memset(stats, 0, sizeof(*stats)); - return static_cast(0x8095F006); + return ErrorInvalidId; } auto& pool = found->second->pool; std::lock_guard poolLock(pool.mutex); @@ -757,7 +778,7 @@ int APS5_VABI sceSslFreeCaCerts(int sslCtxId, SceSsl::CaCerts* certificates) { } int APS5_VABI sceSslCreateConnection(int sslCtxId, int sockId, const char* host) { - if (!host) throw std::invalid_argument("sceSslCreateConnection: null host"); + if (!host) return ErrorInvalidValue; std::lock_guard lock(State().mutex); if (!State().contexts.contains(sslCtxId)) return ErrorInvalidId; auto& state = State(); @@ -771,58 +792,74 @@ int APS5_VABI sceSslCreateConnection(int sslCtxId, int sockId, const char* host) int APS5_VABI sceSslDeleteConnection(int sslConnectionId) { std::shared_ptr session; std::lock_guard lock(State().mutex); - const auto found = State().connections.find(sslConnectionId); - if (found == State().connections.end()) throw std::invalid_argument("sceSslDeleteConnection: unknown connection"); - session = std::move(found->second.session); - State().connections.erase(found); + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + session = std::move(connection->session); + State().connections.erase(sslConnectionId); return 0; } int APS5_VABI sceSslSetVerifyCallback(int sslConnectionId, SslVerifyCallback cbfunc, void* userArg) { std::lock_guard lock(State().mutex); - SslConnection& connection = RequireConnection(__func__, sslConnectionId); - connection.verifyCallback = cbfunc; - connection.verifyArgument = userArg; + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + connection->verifyCallback = cbfunc; + connection->verifyArgument = userArg; return 0; } -int APS5_VABI sceSslConnect(int sslConnectionId) { - std::shared_ptr session; +} + +namespace { + +int RunCallback(Session& session, std::unique_lock& sessionLock, SslVerifyCallback callback, void* argument) { + session.verifying = true; + sessionLock.unlock(); + const int decision = callback(session.contextHandle, session.failedOptions, session.peerHandles.data(), + static_cast(session.peerHandles.size()), argument); + sessionLock.lock(); + session.verifying = false; + return decision < 0 ? ErrorCallbackCheck : 0; +} + +int Establish(const char* function, int sslConnectionId, std::shared_ptr& session) { SslVerifyCallback callback = nullptr; void* callbackArgument = nullptr; { auto& state = State(); std::lock_guard lock(state.mutex); - auto& connection = RequireConnection(__func__, sslConnectionId); - if (!connection.session) { - auto context = state.contexts.at(connection.context); + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (!connection->session) { + auto context = state.contexts.at(connection->context); std::shared_ptr owner; std::uintptr_t native = 0; - if (!KernelLookupSocketNative_nid_no_patch(connection.socket, &owner, &native)) - throw std::invalid_argument("sceSslConnect: descriptor " + std::to_string(connection.socket) + " is not a socket"); + if (!KernelLookupSocketNative_nid_no_patch(connection->socket, &owner, &native)) return ErrorNetBadDescriptor; PoolScope scope(context->pool); - session = std::make_shared(std::move(context), std::move(owner), native); - session->Configure(connection.host, connection.alpn, connection.verifyOptions); - connection.session = session; + session = std::make_shared(connection->context, std::move(context), std::move(owner), native); + session->Configure(connection->host, connection->alpn, connection->verifyOptions); + connection->session = session; } else { - session = connection.session; + session = connection->session; } - callback = connection.verifyCallback; - callbackArgument = connection.verifyArgument; + callback = connection->verifyCallback; + callbackArgument = connection->verifyArgument; } std::unique_lock sessionLock(session->mutex); - if (session->verified && session->verifyResult != 0) return session->verifyResult; - if (session->closed) throw std::logic_error("sceSslConnect: the connection was closed"); - if (session->verified) return 0; - if (session->verifying) throw std::logic_error("sceSslConnect: the verify callback is running"); - int code = 0; + if (session->verifying) throw std::logic_error(std::string(function) + ": the verify callback is running"); + if (session->verified) { + if (session->verifyResult == ErrorCallbackCheck && callback) session->verifyResult = RunCallback(*session, sessionLock, callback, callbackArgument); + return session->verifyResult; + } + if (session->gone) return 0; { PoolScope scope(session->context->pool); if (!session->handshaken) { const int result = mbedtls_ssl_handshake(&session->ssl); - if (result == MBEDTLS_ERR_SSL_WANT_READ || result == MBEDTLS_ERR_SSL_WANT_WRITE) { - if (const int pending = session->Code(result)) return pending; - session->Fail(__func__, result); + if (result == MBEDTLS_ERR_SSL_WANT_READ || result == MBEDTLS_ERR_SSL_WANT_WRITE) return session->Code(result, Operation::Connect); + if (result == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE) { + session->gone = true; + return ErrorFatalAlert; } if (result != 0) { { @@ -830,55 +867,85 @@ int APS5_VABI sceSslConnect(int sslConnectionId) { const auto found = State().connections.find(sslConnectionId); if (found != State().connections.end() && found->second.session == session) found->second.session.reset(); } - if (const int failure = session->Code(result)) return failure; - session->Fail(__func__, result); + if (const int failure = session->Code(result, Operation::Connect)) return failure; + session->Fail(function, result); } session->handshaken = true; const char* selected = mbedtls_ssl_get_alpn_protocol(&session->ssl); session->alpnSelected = selected ? selected : ""; } - code = session->VerifyCode(); + session->Verify(); + if (callback && session->verifyResult == ErrorInvalidCert) + throw std::runtime_error(std::string(function) + ": the chain failed a check no verify option names; what the console passes to the verify callback is unknown"); if (callback) RegisterPeer(*session); } - if (callback) { - session->verifying = true; - sessionLock.unlock(); - const int decision = callback(sslConnectionId, static_cast(code), session->peerHandles.data(), - static_cast(session->peerHandles.size()), callbackArgument); - sessionLock.lock(); - session->verifying = false; - code = decision < 0 ? ErrorCallbackCheck : 0; - } + if (callback) session->verifyResult = RunCallback(*session, sessionLock, callback, callbackArgument); session->verified = true; - session->verifyResult = code; - if (code != 0 && !session->closed) { + if (session->verifyResult != 0 && session->verifyResult != ErrorCallbackCheck) { PoolScope scope(session->context->pool); mbedtls_ssl_close_notify(&session->ssl); session->closed = true; } - return code; + return session->verifyResult; +} + +} + +extern "C" { + +int APS5_VABI sceSslConnect(int sslConnectionId) { + std::shared_ptr session; + return Establish(__func__, sslConnectionId, session); } int APS5_VABI sceSslRead(int sslConnectionId, void* buf, size_t len, int flags) { - if (flags != 0 && flags != ReadPartial) throw std::invalid_argument("sceSslRead: unsupported flags " + std::to_string(flags)); if (!buf && len) throw std::invalid_argument("sceSslRead: null buffer"); - const auto session = RequireSession(__func__, sslConnectionId); + std::shared_ptr session; + if (const int code = Establish(__func__, sslConnectionId, session)) { + if (session) { + std::lock_guard sessionLock(session->mutex); + RequireUsable(__func__, *session); + } + return code; + } std::lock_guard sessionLock(session->mutex); - RequireVerified(__func__, *session); + if (session->gone || session->closed) return 0; + if (session->reset) return ErrorNetReset; PoolScope scope(session->context->pool); - const int result = mbedtls_ssl_read(&session->ssl, static_cast(buf), std::min(len, INT_MAX)); - if (result >= 0) return result; - if (result == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) return 0; - if (const int code = session->Code(result)) return code; - session->Fail(__func__, result); + auto* bytes = static_cast(buf); + const auto total = std::min(len, INT_MAX); + std::size_t received = 0; + while (received < total) { + const int result = mbedtls_ssl_read(&session->ssl, bytes + received, total - received); + if (result > 0) { + received += static_cast(result); + if (flags != 0) break; + continue; + } + if (result == 0 || result == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY || result == MBEDTLS_ERR_SSL_CONN_EOF) break; + const int code = session->Code(result, Operation::Read); + if (code == 0) session->Fail(__func__, result); + if (received) break; + return code; + } + return static_cast(received); } int APS5_VABI sceSslWrite(int sslConnectionId, void* buf, size_t len, int flags) { - if (flags != 0) throw std::invalid_argument("sceSslWrite: unsupported flags " + std::to_string(flags)); + if (flags != 0) return ErrorInvalidValue; if (!buf && len) throw std::invalid_argument("sceSslWrite: null buffer"); - const auto session = RequireSession(__func__, sslConnectionId); + std::shared_ptr session; + if (const int code = Establish(__func__, sslConnectionId, session)) { + if (session) { + std::lock_guard sessionLock(session->mutex); + RequireUsable(__func__, *session); + } + return code; + } std::lock_guard sessionLock(session->mutex); - RequireVerified(__func__, *session); + if (session->gone) return ErrorSocketClosed; + if (session->closed) throw std::logic_error("sceSslWrite: the connection was closed; the console's behaviour is unknown"); + if (session->reset) return ErrorNetPipe; PoolScope scope(session->context->pool); const auto* bytes = static_cast(buf); const auto total = std::min(len, INT_MAX); @@ -886,9 +953,10 @@ int APS5_VABI sceSslWrite(int sslConnectionId, void* buf, size_t len, int flags) while (written < total) { const int result = mbedtls_ssl_write(&session->ssl, bytes + written, total - written); if (result < 0) { + const int code = session->Code(result, Operation::Write); + if (code == 0) session->Fail(__func__, result); if (written) break; - if (const int code = session->Code(result)) return code; - session->Fail(__func__, result); + return code; } written += static_cast(result); } @@ -899,15 +967,20 @@ int APS5_VABI sceSslClose(int sslConnectionId) { std::shared_ptr session; { std::lock_guard lock(State().mutex); - session = RequireConnection(__func__, sslConnectionId).session; + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + session = connection->session; } if (!session) return 0; std::lock_guard sessionLock(session->mutex); - if (session->closed) return 0; - session->closed = true; - if (!session->handshaken) return 0; + if (session->gone || session->closed || !session->handshaken) return 0; + if (session->reset) return ErrorNetReset; PoolScope scope(session->context->pool); - mbedtls_ssl_close_notify(&session->ssl); + const int result = mbedtls_ssl_close_notify(&session->ssl); + if (result == MBEDTLS_ERR_SSL_WANT_WRITE || result == MBEDTLS_ERR_SSL_WANT_READ) return session->Code(result, Operation::Close); + if (result == MBEDTLS_ERR_NET_CONN_RESET) return session->Code(result, Operation::Close); + if (result != 0) session->Fail(__func__, result); + session->closed = true; return 0; } @@ -925,7 +998,6 @@ int APS5_VABI sceSslGetSerialNumber(int sslCtxId, void* sslCert, unsigned char* return ErrorInvalidArg; } std::memcpy(sboData, crt->serial.p, reqLen); - *sboLen = reqLen; return 0; } @@ -942,13 +1014,19 @@ int APS5_VABI sceSslFreeSslCertName(int sslCtxId, void* certName) { int APS5_VABI sceSslEnableVerifyOption(int sslConnectionId, std::uint32_t option) { std::lock_guard lock(State().mutex); - RequireConnection(__func__, sslConnectionId).verifyOptions |= option; + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (option & ~VerifyOptionMask) return ErrorInvalidValue; + connection->verifyOptions |= option; return 0; } int APS5_VABI sceSslDisableVerifyOption(int sslConnectionId, std::uint32_t option) { std::lock_guard lock(State().mutex); - RequireConnection(__func__, sslConnectionId).verifyOptions &= ~option; + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (option & ~VerifyOptionMask) return ErrorInvalidValue; + connection->verifyOptions &= ~option; return 0; } @@ -967,16 +1045,19 @@ const char* APS5_VABI sceSslGetAlpnSelected(int sslConnectionId) { int APS5_VABI sceSslSetAlpn(int sslConnectionId, const char* proto) { if (!proto) throw std::invalid_argument("sceSslSetAlpn: null protocol list"); std::lock_guard lock(State().mutex); - RequireConnection(__func__, sslConnectionId).alpn = proto; + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (!*proto) return ErrorBadLength; + connection->alpn = proto; return 0; } int APS5_VABI sceSslSetMinSslVersion(int sslConnectionId, int version) { - if (version < SslVersionNone || version > SslVersionTls12) { - throw std::invalid_argument("sceSslSetMinSslVersion: unknown version " + std::to_string(version)); - } std::lock_guard lock(State().mutex); - RequireConnection(__func__, sslConnectionId).minimumVersion = version; + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (version < SslVersionSsl30 || version > SslVersionTls12) return ErrorInvalidValue; + connection->minimumVersion = version; return 0; } diff --git a/core/libs/prx/libSceSysmodule/Export.cpp b/core/libs/prx/libSceSysmodule/Export.cpp index 94f6df2150..78b4774cfa 100644 --- a/core/libs/prx/libSceSysmodule/Export.cpp +++ b/core/libs/prx/libSceSysmodule/Export.cpp @@ -272,6 +272,28 @@ int APS5_VABI sceSysmoduleUnloadModuleInternal(std::uint32_t id) { return 0; } +int APS5_VABI sceSysmoduleUnloadModuleInternalWithArg(std::uint32_t id, int argc, void* argv, std::uint64_t unk, int* ret) { + (void)argc; + (void)argv; + (void)unk; + if ((id & 0x7fffffffu) == 0) { + return SCE_SYSMODULE_ERROR_INVALID_VALUE; + } + if (!findModuleName(id)) { + throw std::runtime_error(std::string("sceSysmoduleUnloadModuleInternalWithArg: unknown id ") + std::to_string(id)); + } + std::lock_guard lock(gMutex); + auto it = gLoadCount.find(id); + if (it == gLoadCount.end() || it->second < 1) { + return SCE_SYSMODULE_ERROR_UNLOADED; + } + it->second--; + if (ret) { + *ret = 0; + } + return 0; +} + int APS5_VABI sceSysmoduleIsLoadedInternal(std::uint32_t id) { if ((id & 0x7fffffffu) == 0) { return SCE_SYSMODULE_ERROR_INVALID_VALUE; @@ -287,4 +309,34 @@ int APS5_VABI sceSysmoduleIsLoadedInternal(std::uint32_t id) { return 0; } +int APS5_VABI sceSysmoduleIsCalledFromSysModule() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysmoduleIsCameraPreloaded() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysmoduleLoadModuleByNameInternal(std::uint8_t* fname, std::uint64_t a1, std::uint64_t a2, std::uint64_t a3, std::uint64_t a4, std::uint64_t a5) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysmoduleMapLibcForLibkernel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysmodulePreloadModuleForLibkernel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysmoduleUnloadModuleByNameInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceSystemGesture/Export.cpp b/core/libs/prx/libSceSystemGesture/Export.cpp index 404c9d9d67..516982e980 100644 --- a/core/libs/prx/libSceSystemGesture/Export.cpp +++ b/core/libs/prx/libSceSystemGesture/Export.cpp @@ -200,12 +200,11 @@ int APS5_VABI sceSystemGestureGetPrimitiveTouchEventsCount(int32_t gesture_handl } int APS5_VABI sceSystemGestureGetTouchEventByEventID(int32_t gesture_handle, const SystemGestureTouchRecognizer* recognizer, uint32_t eventId, SystemGestureTouchEvent* event) { - (void)gesture_handle; - (void)recognizer; - (void)eventId; - (void)event; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)eventId; + (void)event; + if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + return SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY; } int APS5_VABI sceSystemGestureGetTouchEventByIndex(int32_t gesture_handle, const SystemGestureTouchRecognizer* recognizer, uint32_t index, SystemGestureTouchEvent* event) { @@ -311,4 +310,9 @@ int APS5_VABI sceSystemGestureUpdateTouchRecognizerRectangle(int32_t gesture_han return 0; } +int APS5_VABI sceSystemGestureDebugGetVersion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 8c533a1553..645da47bd0 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceSystemService SHARED EXCLUDE_FROM_ALL Export.cpp LoadExec.cpp) +add_library(libSceSystemService SHARED EXCLUDE_FROM_ALL Export.cpp PendingExports.cpp LoadExec.cpp) target_include_directories(libSceSystemService PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceSystemService PRIVATE libSceAgcDriver) configure_windows_unwind(libSceSystemService) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp new file mode 100644 index 0000000000..3fe8d5dfc5 --- /dev/null +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -0,0 +1,2390 @@ +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAppMessagingClearEventFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingReceiveMsg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingSendMsg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingSendMsgToShellCore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingSendMsgToShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingSetEventFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingTryGetEventFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingTryReceiveMsg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilAcquireCpuBudgetOfExtraAudioDevices() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilAcquireCpuBudgetOfImeForBigApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilAcquireCpuBudgetOfInGameStore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilActivateCdlg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilAddLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilBlockAppSuspend() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilBlockingGetEventForDaemon() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilContinueApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilCrashSyscore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilDeactivateCdlg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilDeclareReadyForSuspend() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilDisableSuspendNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilEnableSuspendNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilFinishSpecialResume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilForceKillApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilForceKillLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppCategory() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppCategoryType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppFocusedAppStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppIdListOfBigApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppIdOfRunningBigApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppLaunchedUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppStatusList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppStatusListForShellUIReboot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetCdlgExec() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetCdlgType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetCoredumpState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetDbgExecutablePath() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetEventForDaemon() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetEventForShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetGpuCrashFullDumpAppStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetLocalProcessStatusList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetParentSocket() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetResultKillApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetResultLaunchAppByTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetVideoFinalizeState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsActiveCdlg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsAppLaunched() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsAppSuspended() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsCdlgActivated() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsCpuBudgetOfExtraAudioDevicesAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsHostBoot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsPs2Emu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsShellUiFgAndGameBgCpuMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsSpeculativeLaunch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilKickCoredumpOnlyProcMem() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilKillApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilKillAppWithReason() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilKillLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilLaunchApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilLoadExec() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilNotifyCoredumpRequestEnd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilNotifyCoredumpRequestProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilNotifyVshReady() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRaiseException() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRaiseExceptionLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRegisterCdlgSharedMemoryName() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRegisterDaemon() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRegisterShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilReleaseCpuBudgetOfExtraAudioDevices() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilReleaseCpuBudgetOfImeForBigApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilReleaseCpuBudgetOfInGameStore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilResumeApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilResumeLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSetAppFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSetCdlgExec() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSetControllerFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSetControllerFocusPermission() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilStartKillApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilStartLaunchAppByTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSuspendApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSuspendBackgroundApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSuspendLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilTryBlockAppSuspend() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilUnblockAppSuspend() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilUnregisterCdlgSharedMemoryName() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilUnregisterDaemon() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilUnregisterShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilAccessibilityZoomLock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilAccessibilityZoomUnlock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilAcquireBgmCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilAcquireRemotePlayCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilAcquireSharePlayCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateAbort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateIsActivated() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateRecordActivation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateStartAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilChangeRunLevel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilChangeToStaffModeForIDU() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilCheckerAbort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilCleanupCrashReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilClearAppData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilClearPs4AppCategoryDefaultsListUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilClearPsnAccountInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilCrashReportRequestCancel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDeclareBeginOfExternalStorageAppMove() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDeclareEndOfExternalStorageAppMove() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDeleteDiscInstalledTitleWorkaroundFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDeleteDownloadedHidConfigFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDeleteDownloadedNetEvConfigFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDeleteDownloadedTitleWorkaroundFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDoFsck() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDownloadHidConfigFileFromServer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDownloadNetEvConfigFileFromServer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDownloadTitleWorkaroundFileFromServer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilEndUmmDictation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilEnterPowerLockSection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilExecuteCrashReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilExfatFormatExternalHdd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilExitApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilExitMiniApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilExitMiniAppWithValue() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilFormatExternalHdd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilFormatHddForRestore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppEnableTTS() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppEnterButtonAssign() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchTypeInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamInt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamIntByBudgetType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamStringByBudgetType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAutoPowerDownRemainingSeconds() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetBasicProductShape() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCheckerString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCheckerStringEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCloudClientStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportCoreFileSetSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportFilterInfoStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportInfoForBoot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportInfoForBootStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportInfoStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportUploadStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetDeviceIndexBehavior() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetDeviceIndexBehaviorWithTimeout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetEffectiveTotalSizeOfUserPartition() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetFreeSizeOfAvContentsTmp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetFreeSizeOfUserPartition() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetFsckProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetGameLiveStreamingStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetGnmCompositorOnScreenProfilerFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetGpuLoadEmulationMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetGpuLoadEmulationModeByAppId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetHidConfigFileInfoString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetHidConfigFileString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetHidConfigName() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetHidConfigNum() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetIDUMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetImposeMenuFlagForPs2Emu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetManifestFileStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetNeedSizeOfAppContent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetNetEvConfigFileInfoString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetOutOfVrPlayZoneWarning() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPapcGamePcl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPbtcUserInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPlatformPrivacyDefinitionEventData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetProgressOfFormatExternalHdd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetProgressOfFsck() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPs4AppCategoryDefaultsListId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPs4AppCategoryForTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPsStoreIconLayout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPsStoreIconState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPsnAccountInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetRemotePlayStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetRunLevel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSharePlayStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSocialScreenStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSplashScreenState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSupportSiteURL() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSuspendConfirmationDialogFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSystemBGState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSystemBGWaveColor() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSystemBGWaveState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetTelemetryToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetTitleWorkaroundFileInfoString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetTitleWorkaroundFileString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetUIStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetUmmStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetUserFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetUserIdOfMorpheusUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetVMStatsForTelemetry() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetVersionNumberOfCameraCalibrationData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGoBackToKratosCurrentSessionGame() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilHideBlocksFromUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIncrementVersionNumberOfCameraCalibrationData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsAccessibilityZoomLocked() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsBgmCpuBudgetAcquired() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsBgmCpuBudgetAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsBgmPlaying() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsExternalStorageAppMoveInProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsEyeToEyeDistanceAdjusted() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsGameLiveStreamingOnAir() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsIDUState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsImposeScreenOverlaid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsInSystemSuspendBlackList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsInternalKratosUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsKilledOrSuspendedByLogout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsNeededCrashReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsPowerSaveAlertRequested() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsRemotePlayCpuBudgetAcquired() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsScreenSaverOn() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsSharePlayCpuBudgetAcquired() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsShowCrashReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsTemperatureDanger() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsTitleWorkaroundEnabled() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsUsbMassStorageMounted() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilLaunchByUri() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilLeavePowerLockSection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilMakeManifestFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilMountAppRight() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilMountDownloadDataForShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilMountHddForBackup() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilMountHddForRestore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNavigateToAnotherApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNavigateToGoHome() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNavigateToLaunchedApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotificationCancelForIDU() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotificationRequestedForIDU() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyBgmCoreTermination() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyFsReadError() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyImeStatusChanged() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyPsnAccountInfoReceived() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifySystemSuspendTelemetryInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPfAuthClientConsoleTokenClearCache() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPostActivityForPsNow() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPostErrorLog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPostLaunchConfirmResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPostPsmEventToShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPreNotifyOfGameLiveStreaming() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPreNotifyOfRemotePlay() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPreNotifyOfSharePlay() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilReleaseBgmCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilReleaseRemotePlayCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilReleaseSharePlayCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilReportSessionErrorToGaikaiController() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilReportUnexpectedFatalErrorToSystemTelemetry() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRequestCameraCalibration() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRequestEjectDevice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRequestRebootApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRequestShutdown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilResetAutoPowerDownTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilResetBgdcConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetAppData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetBgmProhibition() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetDeviceIndexBehavior() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGameLiveStreamingOnAirFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGameLiveStreamingStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGnmCompositorOnScreenProfilerFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGpuLoadEmulationMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGpuLoadEmulationModeByAppId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetIDUMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetImposeStatusFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetPsStoreIconLayout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetPsStoreIconState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetRemotePlayStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSharePlayStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSkipUpdateCheck() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSocialScreenStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSplashScreenState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSystemBGState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSystemBGWaveColor() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSystemBGWaveState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetUIStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetUserFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilShowCriticalErrorDialog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilShowErrorDialog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilShowErrorDialogWithFormatArgs() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilShowErrorDialogWithParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilShowPsUnderLockIndicator() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSignalUserInput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilStartPsNowGame() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilStopPsNowGame() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTalkKeyEventReceived() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTestBusTransferSpeed() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTickHeartBeat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTriggerPapcUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTurnOffScreenSaver() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilUnmountAppRight() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilUnmountDownloadDataForShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilUnmountHddForBackup() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilUnmountHddForRestore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreutilGetCrashReportProcessInformation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemModalDialogClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemModalDialogOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAcquireBgmCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAcquireFb0() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcAbort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcIsActivated() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoft() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftAbort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftIsActivated() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2Abort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2GetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2Init() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2IsActivated() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2Start() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2Term() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAddLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAddLocalProcessForJvm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAddLocalProcessForPs2Emu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAppGetAppInstallStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeAcpClock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeClock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeCpuClock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeCpuPstate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeGpuClock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemPstate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemoryClock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemoryClockToBaseMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemoryClockToDefault() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemoryClockToMultiMediaMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeNumberOfGpuCu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeSamuClock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeUvdClock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeVceClock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceDeclareReadyForSuspend() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceDisablePartyVoice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceDisablePersonalEyeToEyeDistanceSetting() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceDisableSuspendConfirmationDialog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceDisableSuspendNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceDisableVoiceRecognition() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceEnablePersonalEyeToEyeDistanceSetting() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceEnableSuspendConfirmationDialog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceEnableSuspendNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppCategoryType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppFocusedAppStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppIdListOfBigApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppLaunchedUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetDbgExecutablePath() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetEventForDaemon() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetGpuLoadEmulationMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetLocalProcessStatusList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetMainAppTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetPSButtonEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetParentSocket() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetParentSocketForJvm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetParentSocketForPs2Emu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetParentSocketForPsmKit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetPlatformPrivacyDefinitionData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetPlatformPrivacyDefinitionVersion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetPlatformPrivacySetting() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetRecentLotusPushedButton() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetRemoteplayStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetRenderingMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetSubAppTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetSubAppTitleIdForSP() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetTitleWorkaroundInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetUIStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetVersionNumberOfCameraCalibrationData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceInitializeForShellCore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceInvokeAppLaunchLink() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsAppSuspended() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsBgmCpuBudgetAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsBgmPlaying() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsEyeToEyeDistanceAdjusted() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsGameLiveStreamingOnAir() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsScreenSaverOn() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsShellUiFgAndGameBgCpuMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceKillLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceKillLocalProcessForJvm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceKillLocalProcessForPs2Emu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchEventDetails() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchStore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchTournamentList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchTournamentsTeamProfile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchUdsApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchWebApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLoadExecVideoServiceWebApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceNavigateToAnotherApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceNavigateToGoBack() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceNavigateToGoBackWithValue() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceNavigateToGoHome() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceNotifyBgmCoreTermination() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceRaiseExceptionLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceReenablePartyVoice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceReenableVoiceRecognition() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceRegisterDaemon() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceReleaseBgmCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceReleaseFb0() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceRequestCameraCalibration() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceRequestPowerOff() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceRequestReboot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceRequestToChangeRenderingMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceResumeLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSaveVideoToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSetControllerFocusPermission() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSetGpuLoadEmulationMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSetOutOfVrPlayAreaFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSetOutOfVrPlayZoneWarning() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSetPowerSaveLevel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceShowClosedCaptionAdvancedSettings() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceShowClosedCaptionSettings() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceShowErrorDialog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceShowEyeToEyeDistanceSetting() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceShowImposeMenuForPs2Emu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSuspendBackgroundApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSuspendLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceTelemetrySetData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceTelemetrySetData2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceTickVideoPlayback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceTurnOffScreenSaver() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageGetDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageGetDeviceList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageGetdentsClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageGetdentsOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageGetdentsRead() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageIsExist() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRegisterCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRegisterCallbackForMapAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRegisterCallbackForMapUnavailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRequestMap() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRequestMapWSB() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRequestUnmap() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageUnregisterCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageUnregisterCallbackForMapAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageUnregisterCallbackForMapUnavailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrCancelShutdownTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrEnterMediaPlaybackMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrEnterStandby() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrExtendShutdownTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrExtendShutdownTimerForPostAutoUpdateProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrGetCurrentState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrGetTriggerCode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrIsBdDriveReady() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrIsGpuPerformanceNormal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrIsShellUIShutdownInProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrIsStandbyModeEnabled() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrLeaveMediaPlaybackMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrNotifySystemSuspendResumeProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrReboot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrRequestToKeepMainOnStandby() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrSendCecOneTouchPlayCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrStartRebootTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrStartShutdownTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrStartStadbyTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrStartVshAutoUpdateTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrTickMusicPlayback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrTickPartyChat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrTurnOff() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrVshAutoUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrWaitVshAutoUpdateVerifyDone() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrWakeUp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceUlt/CMakeLists.txt b/core/libs/prx/libSceUlt/CMakeLists.txt index e8cb567f4d..25fa2dad49 100644 --- a/core/libs/prx/libSceUlt/CMakeLists.txt +++ b/core/libs/prx/libSceUlt/CMakeLists.txt @@ -3,6 +3,19 @@ target_include_directories(libSceUlt PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceUlt) if(BUILD_TESTING) + add_test_executable(guest_ult_condition_tests ${LIBS_INCLUDE_DIR}/tests/GuestUltCondition.cpp) + target_include_directories(guest_ult_condition_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ult_condition_tests PRIVATE libSceUlt libc) + configure_windows_unwind(guest_ult_condition_tests) + foreach(testCase IN ITEMS validation handoff signal-one stress finalize-queued finalize-reacquire) + add_test(NAME guest_ult_condition_${testCase} COMMAND guest_ult_condition_tests ${testCase}) + set_tests_properties(guest_ult_condition_${testCase} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_ult_condition_${testCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + add_test_executable(guest_ult_queue_tests ${LIBS_INCLUDE_DIR}/tests/GuestUltQueue.cpp) target_include_directories(guest_ult_queue_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_ult_queue_tests PRIVATE libSceUlt libc) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 506333c24e..7d2af42297 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -23,6 +23,7 @@ namespace { std::mutex gMutex; std::unordered_map> gMutexes; +std::unordered_map> gConditions; std::unordered_map> gSemaphores; std::unordered_map gResourcePools; std::unordered_map> gQueueDataPools; @@ -145,6 +146,13 @@ int APS5_VABI sceUltFinalize() { state->_alive = false; mutexes.push_back(state); } + for (const auto& [object, condition] : gConditions) { + std::lock_guard stateLock(condition->_mutex->_mutex); + condition->_alive = false; + --condition->_mutex->_conditions; + for (auto* waiter : condition->_queue) waiter->_available.notify_one(); + } + gConditions.clear(); gMutexes.clear(); gResourcePools.clear(); for (auto& entry : gQueues) { @@ -208,7 +216,7 @@ int APS5_VABI sceUltMutexDestroy(void* mutex) { } auto state = it->second; std::lock_guard stateLock(state->_mutex); - if (state->_lockCount != 0 || state->_waiters != 0) { + if (state->_lockCount != 0 || state->_waiters != 0 || state->_conditions != 0 || state->_conditionWaiters != 0) { return ULT_ERROR_BUSY; } state->_alive = false; @@ -276,6 +284,101 @@ int APS5_VABI sceUltMutexUnlock(void* mutex) { return ULT_OK; } +int APS5_VABI _sceUltConditionVariableCreate(void* condition, const char*, void* mutex, const void* optParam, std::uint32_t) { + if (condition == nullptr || mutex == nullptr) return ULT_ERROR_NULL; + if (optParam != nullptr) throw std::runtime_error("ULT condition variable options are not implemented"); + std::lock_guard lock(gMutex); + const auto mutexIt = gMutexes.find(mutex); + if (mutexIt == gMutexes.end()) return ULT_ERROR_INVALID; + if (gConditions.contains(condition)) return ULT_ERROR_STATE; + auto state = std::make_shared(); + state->_mutex = mutexIt->second; + std::lock_guard stateLock(state->_mutex->_mutex); + if (!state->_mutex->_alive) return ULT_ERROR_STATE; + gConditions.emplace(condition, state); + ++state->_mutex->_conditions; + return ULT_OK; +} + +int APS5_VABI sceUltConditionVariableDestroy(void* condition) { + std::lock_guard lock(gMutex); + const auto it = gConditions.find(condition); + if (it == gConditions.end()) return condition == nullptr ? ULT_ERROR_NULL : ULT_ERROR_STATE; + auto state = it->second; + std::lock_guard stateLock(state->_mutex->_mutex); + if (state->_waiters != 0) return ULT_ERROR_BUSY; + state->_alive = false; + --state->_mutex->_conditions; + gConditions.erase(it); + return ULT_OK; +} + +int APS5_VABI sceUltConditionVariableSignal(void* condition) { + std::shared_ptr state; + { + std::lock_guard lock(gMutex); + const auto it = gConditions.find(condition); + if (it == gConditions.end()) return condition == nullptr ? ULT_ERROR_NULL : ULT_ERROR_STATE; + state = it->second; + } + std::lock_guard stateLock(state->_mutex->_mutex); + if (!state->_alive || !state->_mutex->_alive) return ULT_ERROR_STATE; + if (!state->_queue.empty()) { + auto* waiter = state->_queue.front(); + state->_queue.pop_front(); + waiter->_signaled = true; + waiter->_available.notify_one(); + } + return ULT_OK; +} + +int APS5_VABI sceUltConditionVariableWait(void* condition) { + std::shared_ptr state; + { + std::lock_guard lock(gMutex); + const auto it = gConditions.find(condition); + if (it == gConditions.end()) return condition == nullptr ? ULT_ERROR_NULL : ULT_ERROR_STATE; + state = it->second; + } + const auto held = state->_mutex; + const auto owner = std::this_thread::get_id(); + 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"); + UltConditionWaiter waiter; + state->_queue.push_back(&waiter); + ++state->_waiters; + ++held->_conditionWaiters; + held->_lockCount = 0; + held->_owner = {}; + held->_available.notify_one(); + try { + waiter._available.wait(lock, [&] { return waiter._signaled || !state->_alive || !held->_alive; }); + if (held->_alive && state->_alive) { + ++held->_waiters; + try { + held->_available.wait(lock, [&] { return !held->_alive || held->_lockCount == 0; }); + } catch (...) { + --held->_waiters; + throw; + } + --held->_waiters; + } + } catch (...) { + state->_queue.remove(&waiter); + --state->_waiters; + --held->_conditionWaiters; + throw; + } + state->_queue.remove(&waiter); + --state->_waiters; + --held->_conditionWaiters; + if (!held->_alive || !state->_alive) return ULT_ERROR_STATE; + held->_owner = owner; + held->_lockCount = 1; + return ULT_OK; +} + int APS5_VABI sceUltWaitingQueueResourcePoolGetWorkAreaSize(std::uint32_t numThreads, std::uint32_t numSyncObjects) { return static_cast(alignUp(static_cast(numThreads + numSyncObjects) * 256u, 8u)); } @@ -713,4 +816,214 @@ int APS5_VABI _sceUltUlthreadCreate(void* ulthread, const char* name, UltUlthrea return sceUltUlthreadCreate(ulthread, name, entry, arg, context, sizeContext, runtime, optParam, buildVersion); } +int APS5_VABI sceUltConditionVariableSignalAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetConditionVariableInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetMutexInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetQueueDataResourcePoolInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetQueueInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetReaderWriterLockInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetSemaphoreInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetUlthreadInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetUlthreadRuntimeInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetWaitingQueueResourcePoolInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltMutexTryLock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltReaderWriterLockDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltReaderWriterLockLockRead() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltReaderWriterLockLockWrite() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltReaderWriterLockTryLockRead() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltReaderWriterLockTryLockWrite() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltReaderWriterLockUnlockRead() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltReaderWriterLockUnlockWrite() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadExit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadGetSelf() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadYield() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltAbstractThreadFactoryCreateThread() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltAbstractThreadFactoryGetNumMaxThreads() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltAbstractThreadFactoryJoinThread() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltKernelThreadFactoryFinalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltKernelThreadFactoryGetFactoryPointer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltKernelThreadFactoryInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltModuleFinalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltModuleInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadFactoryFinalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadFactoryGetFactoryPointer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadFactoryGetWorkAreaSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadFactoryInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltConditionVariableCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltConditionVariableOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltQueueDataResourcePoolOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltQueueOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltReaderWriterLockCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltReaderWriterLockOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltSemaphoreOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltWaitingQueueResourcePoolOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceUlt/SceUltTypes.hpp b/core/libs/prx/libSceUlt/SceUltTypes.hpp index 995972b952..0da430382c 100644 --- a/core/libs/prx/libSceUlt/SceUltTypes.hpp +++ b/core/libs/prx/libSceUlt/SceUltTypes.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -25,11 +26,25 @@ struct UltMutexState { std::thread::id _owner{}; std::uint32_t _lockCount = 0; std::uint32_t _waiters = 0; + std::uint32_t _conditions = 0; + std::uint32_t _conditionWaiters = 0; bool _alive = true; std::uint32_t _attribute = 0; void* _waitingPool = nullptr; }; +struct UltConditionWaiter { + std::condition_variable _available; + bool _signaled = false; +}; + +struct UltConditionState { + std::shared_ptr _mutex; + std::list _queue; + std::uint32_t _waiters = 0; + bool _alive = true; +}; + struct UltSemaphoreState { std::mutex _mutex; std::condition_variable _available; diff --git a/core/libs/prx/libSceUsbStorage/Export.cpp b/core/libs/prx/libSceUsbStorage/Export.cpp index befdd3e768..0704602439 100644 --- a/core/libs/prx/libSceUsbStorage/Export.cpp +++ b/core/libs/prx/libSceUsbStorage/Export.cpp @@ -76,4 +76,25 @@ int APS5_VABI sceUsbStorageGetdentsClose(int32_t fd) { return 0; } +int APS5_VABI sceUsbStorageClearFakeWriteProtection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUsbStorageRequestMapWSB(uint32_t device_id, const char* subpath, int32_t flags, + int32_t unknown, char* out_mount_path, int32_t* out_status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUsbStorageSetFakeMapLockForDebug() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUsbStorageSetFakeWriteProtection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 2c276b120f..9c7f9ae6da 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -149,6 +149,15 @@ if(BUILD_TESTING) set_tests_properties(pad_input_mapping PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(pad_output_pacing_tests tests/PadOutputPacing.cpp) + target_include_directories(pad_output_pacing_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(pad_output_pacing_tests PRIVATE libc) + configure_windows_unwind(pad_output_pacing_tests) + add_test(NAME pad_output_pacing COMMAND pad_output_pacing_tests) + if(WIN32) + set_tests_properties(pad_output_pacing PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(BUILD_TESTING) diff --git a/core/libs/prx/libSceVideoOut/include/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index 227fb8df70..dfa068d16d 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -5,6 +5,7 @@ #include "SDL_gamecontroller.h" #include "prx/libScePad/include/KeyboardMouse.hpp" #include "prx/libScePad/include/PadState.hpp" +#include "prx/libSceVideoOut/include/PadOutputPacing.hpp" #include #include "prx/libSceVideoOut/include/Telemetry.hpp" @@ -37,6 +38,8 @@ private: bool outputPending = false; VideoOutTelemetry::PadChangeFilter telemetryFilter; std::chrono::steady_clock::time_point nextRumbleRefresh{}; + LightBarPacer lightBar; + TriggerEffectCache triggerEffect; }; #endif diff --git a/core/libs/prx/libSceVideoOut/include/PadOutputPacing.hpp b/core/libs/prx/libSceVideoOut/include/PadOutputPacing.hpp new file mode 100644 index 0000000000..6061fcf618 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/include/PadOutputPacing.hpp @@ -0,0 +1,63 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_PADOUTPUTPACING_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_PADOUTPUTPACING_HPP + +#include +#include +#include + +class LightBarPacer { +public: + using Clock = std::chrono::steady_clock; + using Color = std::array; + static constexpr std::chrono::milliseconds Interval{30}; + + bool Pending(const Color& color) const { + return !sent || color != last; + } + + bool Due(const Color& color, Clock::time_point now) const { + return Pending(color) && now >= next; + } + + void Result(const Color& color, Clock::time_point now, bool succeeded) { + next = now + Interval; + if (!succeeded) return; + last = color; + sent = true; + } + + void Reset() { + sent = false; + next = {}; + } + +private: + Color last{}; + bool sent = false; + Clock::time_point next{}; +}; + +class TriggerEffectCache { +public: + using Effect = std::array; + + bool NeedsSend(const Effect& effect) const { + return !sent || effect != last; + } + + void Result(const Effect& effect, bool succeeded) { + if (!succeeded) return; + last = effect; + sent = true; + } + + void Reset() { + sent = false; + } + +private: + Effect last{}; + bool sent = false; +}; + +#endif diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index 069a00c174..7c868f3ce2 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -44,6 +44,7 @@ static constexpr int VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE = -2144796642; static constexpr int VIDEO_OUT_BUS_TYPE_MAIN = 0; static constexpr int VIDEO_OUT_BUS_TYPE_OVERLAY = 1; static constexpr int VIDEO_OUT_BUS_TYPE_SUB = 2; +static constexpr int VIDEO_OUT_BUS_TYPE_HMD2_MAIN = 0x20; static constexpr uint32_t VIDEO_OUT_DEFAULT_WIDTH = 1920; static constexpr uint32_t VIDEO_OUT_DEFAULT_HEIGHT = 1080; diff --git a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp index 751d3706fd..35c6c86917 100644 --- a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp +++ b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp @@ -3,9 +3,9 @@ #include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" -#include "prx/libkernel/Time/include/Time.hpp" #include "Decoder/Png.hpp" #include "SDL_vulkan.h" +#include #include #include #include @@ -107,15 +107,15 @@ void DisplayWindow::DrawableSize(std::uint32_t& width, std::uint32_t& height) co void DisplayWindow::UpdateTitle() { require(window != nullptr, "window must exist before updating title"); static const AppTitle title = GetAppTitle_nid_postfix(); - static std::uint64_t fpsStart = sceKernelGetProcessTimeCounter(); + static auto fpsStart = std::chrono::steady_clock::now(); static std::uint64_t fpsFrames = 0; static double currentFps = 0.0; - const auto now = sceKernelGetProcessTimeCounter(); - const auto frequency = sceKernelGetProcessTimeCounterFrequency(); + const auto now = std::chrono::steady_clock::now(); frameNum++; fpsFrames++; - if (now - fpsStart >= frequency * 2) { - currentFps = static_cast(fpsFrames) * static_cast(frequency) / static_cast(now - fpsStart); + const double elapsed = std::chrono::duration(now - fpsStart).count(); + if (elapsed >= 2.0) { + currentFps = static_cast(fpsFrames) / elapsed; fpsStart = now; fpsFrames = 0; } diff --git a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp index e45f4593b1..dc9bb16da1 100644 --- a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp @@ -55,7 +55,8 @@ constexpr auto actions = std::array{ InputAction{"Shake", Pad::InputControl::Shake, Pad::PadButton::None}, InputAction{"ToggleMouse", Pad::InputControl::ToggleMouse, Pad::PadButton::None}, InputAction{"ToggleFullscreen", Pad::InputControl::ToggleFullscreen, Pad::PadButton::None}, - InputAction{"TogglePerformanceOverlay", Pad::InputControl::TogglePerformanceOverlay, Pad::PadButton::None} + InputAction{"TogglePerformanceOverlay", Pad::InputControl::TogglePerformanceOverlay, Pad::PadButton::None}, + InputAction{"Screenshot", Pad::InputControl::Screenshot, Pad::PadButton::None} }; std::string upper(std::string_view value) { @@ -117,6 +118,9 @@ Pad::InputBinding parseBinding(const InputAction& action, std::string_view sourc if (action.control == Pad::InputControl::TogglePerformanceOverlay) { throw std::runtime_error("TogglePerformanceOverlay can only use a keyboard key"); } + if (action.control == Pad::InputControl::Screenshot) { + throw std::runtime_error("Screenshot can only use a keyboard key"); + } return {SDL_SCANCODE_UNKNOWN, parseMouseButton(value), action.control, action.button, 0}; } if (type == "WHEEL") { diff --git a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp index ed1e1de32d..1ef93610d0 100644 --- a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp @@ -263,6 +263,7 @@ Pad::InputRequests Pad::KeyboardMouse::HandleEvent(const SDL_Event& event, Clock if (!matches) continue; if (binding.control == InputControl::ToggleFullscreen && keyboard && down && !pressed[index]) requests.toggleFullscreen = true; if (binding.control == InputControl::TogglePerformanceOverlay && keyboard && down && !pressed[index]) requests.togglePerformanceOverlay = true; + if (binding.control == InputControl::Screenshot && keyboard && down && !pressed[index]) requests.screenshot = true; if (binding.control == InputControl::ToggleMouse && down && !pressed[index]) requests.toggleMouse = true; pressed[index] = down; } @@ -302,6 +303,7 @@ PadInputState Pad::KeyboardMouse::Compose(const PadInputState& controller) const case InputControl::ToggleMouse: break; case InputControl::ToggleFullscreen: break; case InputControl::TogglePerformanceOverlay: break; + case InputControl::Screenshot: break; } } const auto input = stickInput(); diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index 79af66728a..24f9b6f15c 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -80,6 +80,7 @@ static void validateOpenParam(const void* param) { extern "C" { int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* param) try { + if (busType == VIDEO_OUT_BUS_TYPE_HMD2_MAIN) return VIDEO_OUT_ERROR_INVALID_VALUE; validateOpenParam(param); if (userId != 255 && userId != 0) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE"); diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index a48381cf06..7aa248c61c 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -12,6 +12,7 @@ #include "prx/libSceMouse/include/MouseState.hpp" #include "prx/libSceVideoOut/include/DisplayWindow.hpp" #include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libScePad/include/PadState.hpp" #include "prx/libScePad/include/PadInputTypes.hpp" #include "prx/libc/include/General.hpp" @@ -59,6 +60,8 @@ void PadInput::openController(int deviceIndex) { SDL_GameControllerGetNumTouchpads(controller), SDL_GameControllerHasLED(controller) == SDL_TRUE, SDL_GameControllerHasRumbleTriggers(controller) == SDL_TRUE); enableSensors(); outputPending = true; + lightBar.Reset(); + triggerEffect.Reset(); } void PadInput::enableSensors() { @@ -86,10 +89,16 @@ void PadInput::applyOutput() { if (controller == nullptr) return; const auto now = std::chrono::steady_clock::now(); const bool isPs5 = SDL_GameControllerGetType(controller) == SDL_CONTROLLER_TYPE_PS5; - const auto large = isPs5 ? outputState.vibrationLarge : std::max(outputState.vibrationLarge, outputState.audioVibrationLarge); - const auto small = isPs5 ? outputState.vibrationSmall : std::max(outputState.vibrationSmall, outputState.audioVibrationSmall); + const auto large = std::max(outputState.vibrationLarge, outputState.audioVibrationLarge); + const auto small = std::max(outputState.vibrationSmall, outputState.audioVibrationSmall); const bool rumbling = large != 0 || small != 0; const bool triggerRumble = outputState.trigger[0].fallback != 0 || outputState.trigger[1].fallback != 0; + if (SDL_GameControllerHasLED(controller) == SDL_TRUE) { + const LightBarPacer::Color color = outputState.lightBarValid + ? LightBarPacer::Color{outputState.lightBar[0], outputState.lightBar[1], outputState.lightBar[2]} + : LightBarPacer::Color{0, 64, 255}; + if (lightBar.Due(color, now)) lightBar.Result(color, now, SDL_GameControllerSetLED(controller, color[0], color[1], color[2]) == 0); + } if (!outputPending) { if ((rumbling || (triggerRumble && !isPs5)) && now >= nextRumbleRefresh) outputPending = true; else return; @@ -98,17 +107,15 @@ void PadInput::applyOutput() { nextRumbleRefresh = now + std::chrono::milliseconds(700); constexpr Uint32 rumbleMs = 2000; SDL_GameControllerRumble(controller, static_cast(large * 257), static_cast(small * 257), rumbling ? rumbleMs : 0); - if (SDL_GameControllerHasLED(controller) == SDL_TRUE) { - if (outputState.lightBarValid) SDL_GameControllerSetLED(controller, outputState.lightBar[0], outputState.lightBar[1], outputState.lightBar[2]); - else SDL_GameControllerSetLED(controller, 0, 64, 255); - } if (outputState.triggerTouched) { if (isPs5) { - Uint8 effect[47] = {}; + TriggerEffectCache::Effect effect{}; effect[0] = 0x04 | 0x08; - std::memcpy(effect + 10, outputState.trigger[1].effect, 11); - std::memcpy(effect + 21, outputState.trigger[0].effect, 11); - SDL_GameControllerSendEffect(controller, effect, sizeof(effect)); + std::memcpy(effect.data() + 10, outputState.trigger[1].effect, 11); + std::memcpy(effect.data() + 21, outputState.trigger[0].effect, 11); + if (triggerEffect.NeedsSend(effect)) { + triggerEffect.Result(effect, SDL_GameControllerSendEffect(controller, effect.data(), static_cast(effect.size())) == 0); + } } else if (SDL_GameControllerHasRumbleTriggers(controller) == SDL_TRUE) { SDL_GameControllerRumbleTriggers(controller, static_cast(outputState.trigger[0].fallback * 257), static_cast(outputState.trigger[1].fallback * 257), triggerRumble ? rumbleMs : 0); } @@ -230,6 +237,7 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { const auto requests = keyboardMouse.HandleEvent(event, std::chrono::steady_clock::now()); if (requests.toggleFullscreen && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) window.ToggleFullscreen(); if (requests.togglePerformanceOverlay && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) AgcDriverTogglePerformanceOverlay_nid_postfix(); + if (requests.screenshot && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) AgcDriverRequestScreenshot_nid_postfix(); if (requests.toggleMouse) setMouseMode(!keyboardMouse.MouseMode()); if (event.type != SDL_MOUSEMOTION) publish(window.FrameNumber()); } diff --git a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp index ce7f276384..3497443fcc 100644 --- a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp @@ -92,6 +92,24 @@ void performanceOverlayBinding() { require(rejects("TogglePerformanceOverlay = MOUSE:Left\n"), "the performance overlay toggle refuses a mouse button"); } +void screenshotBinding() { + useConfig("Cross = KEY:F\n"); + bool onF12 = false; + for (const auto& binding : Pad::LoadInputMapping().bindings) onF12 = onF12 || (binding.control == Pad::InputControl::Screenshot && binding.key == SDL_SCANCODE_F12); + require(onF12, "F12 takes a screenshot by default"); + useConfig("Screenshot = KEY:F9\n"); + bool onF9 = false; + bool stillOnF12 = false; + for (const auto& binding : Pad::LoadInputMapping().bindings) { + if (binding.control != Pad::InputControl::Screenshot) continue; + onF9 = onF9 || binding.key == SDL_SCANCODE_F9; + stillOnF12 = stillOnF12 || binding.key == SDL_SCANCODE_F12; + } + require(onF9 && !stillOnF12, "the screenshot key moves to another key"); + require(rejects("Screenshot = MOUSE:Left\n"), "the screenshot key refuses a mouse button"); + require(rejects("Screenshot = WHEEL:Up\n"), "the screenshot key refuses the mouse wheel"); +} + void mouseLookDefault() { useConfig("Cross = KEY:F\n"); const auto settings = Pad::LoadInputMapping().settings; @@ -262,6 +280,7 @@ int main() try { settingsParse(); settingsReject(); performanceOverlayBinding(); + screenshotBinding(); mouseLookDefault(); mouseLookParse(); mouseLookReject(); diff --git a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp index eb763d6a25..e4d9c6892f 100644 --- a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp @@ -464,6 +464,12 @@ void testToggleRequests() { const auto overlay = keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F10, true), now); check(overlay.togglePerformanceOverlay && !overlay.toggleFullscreen, "F10 asks to toggle the performance overlay"); check(!keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F10, false), now).togglePerformanceOverlay, "releasing F10 asks for nothing"); + const auto screenshot = keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F12, true), now); + check(screenshot.screenshot && !screenshot.toggleFullscreen && !screenshot.togglePerformanceOverlay, "F12 asks for a screenshot"); + auto repeated = KeyEvent(SDL_SCANCODE_F12, true); + repeated.key.repeat = 1; + check(!keyboardMouse.HandleEvent(repeated, now).screenshot, "a repeated F12 asks for nothing"); + check(!keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F12, false), now).screenshot, "releasing F12 asks for nothing"); } void testDefaultL2() { diff --git a/core/libs/prx/libSceVideoOut/tests/PadOutputPacing.cpp b/core/libs/prx/libSceVideoOut/tests/PadOutputPacing.cpp new file mode 100644 index 0000000000..ab624ef5a4 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/PadOutputPacing.cpp @@ -0,0 +1,89 @@ +#include "prx/libSceVideoOut/include/PadOutputPacing.hpp" + +#include +#include +#include + +namespace { + +using namespace std::chrono_literals; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "pad_output_pacing: %s\n", message); + std::exit(1); +} + +void LightBarSendsChangesOnly() { + LightBarPacer pacer; + const auto start = LightBarPacer::Clock::time_point{} + 1s; + const LightBarPacer::Color blue{0, 64, 255}; + Require(pacer.Due(blue, start), "the first colour is sent at once"); + pacer.Result(blue, start, true); + Require(!pacer.Pending(blue), "a colour that was sent is not sent again"); + Require(!pacer.Due(blue, start + 1s), "an unchanged colour stays quiet"); + const LightBarPacer::Color red{255, 0, 0}; + Require(pacer.Pending(red) && !pacer.Due(red, start + 10ms), "a new colour waits for the interval"); + Require(pacer.Due(red, start + LightBarPacer::Interval), "a new colour goes out once the interval has passed"); +} + +void LightBarRetriesFailures() { + LightBarPacer pacer; + const auto start = LightBarPacer::Clock::time_point{} + 1s; + const LightBarPacer::Color green{0, 255, 0}; + pacer.Result(green, start, false); + Require(pacer.Pending(green), "a colour SDL refused is still pending"); + Require(!pacer.Due(green, start + 1ms), "a refused colour is not retried on the next frame"); + Require(pacer.Due(green, start + LightBarPacer::Interval), "a refused colour is retried after the interval"); + pacer.Result(green, start + LightBarPacer::Interval, true); + Require(!pacer.Pending(green), "a retried colour that was sent is done"); + pacer.Reset(); + Require(pacer.Due(green, start + LightBarPacer::Interval), "a new controller gets the current colour"); +} + +void LightBarAnimationIsPaced() { + LightBarPacer pacer; + const auto start = LightBarPacer::Clock::time_point{} + 1s; + constexpr int frames = 120; + int sent = 0; + LightBarPacer::Color color{}; + for (int frame = 0; frame < frames; ++frame) { + const auto now = start + std::chrono::microseconds(frame * 8333); + color = {static_cast(frame), 32, 32}; + if (!pacer.Due(color, now)) continue; + pacer.Result(color, now, true); + ++sent; + } + Require(sent >= 30 && sent <= 34, "a colour that changes every frame at 120 Hz goes out about 33 times a second"); + const auto end = start + 1s; + Require(pacer.Pending(color), "the last colour of the animation is still owed"); + Require(pacer.Due(color, end + LightBarPacer::Interval), "the last colour of the animation is sent when the interval allows"); +} + +void TriggerEffectRetriesFailures() { + TriggerEffectCache cache; + TriggerEffectCache::Effect effect{}; + effect[0] = 0x0C; + effect[10] = 0x21; + Require(cache.NeedsSend(effect), "the first effect is sent"); + cache.Result(effect, false); + Require(cache.NeedsSend(effect), "an effect SDL refused is sent again"); + cache.Result(effect, true); + Require(!cache.NeedsSend(effect), "an effect that was sent is not repeated"); + auto changed = effect; + changed[21] = 0x26; + Require(cache.NeedsSend(changed), "a changed effect is sent"); + cache.Reset(); + Require(cache.NeedsSend(effect), "a new controller gets the current effect"); +} + +} + +int main() { + LightBarSendsChangesOnly(); + LightBarRetriesFailures(); + LightBarAnimationIsPaced(); + TriggerEffectRetriesFailures(); + std::puts("pad_output_pacing: ok"); + return 0; +} diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index 1434ad7255..0b8b09eced 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -305,6 +305,8 @@ struct Decoder { std::deque ready; std::vector annexB; std::vector parameterSets; + bool lengthPrefixed = false; + bool shortStartCodes = false; bool flushing = false; std::array vp9References{}; Vp9PictureInfo vp9Colors{}; @@ -381,21 +383,45 @@ void OpenCodec(Decoder& decoder) { } } -const std::vector* ToAnnexB(Decoder& decoder, const std::uint8_t* data, std::size_t size) { - if (size >= 4 && data[0] == 0 && data[1] == 0 && (data[2] == 1 || (data[2] == 0 && data[3] == 1))) { - decoder.annexB.assign(data, data + size); - return &decoder.annexB; - } - decoder.annexB.clear(); +bool TilesAsAvcc(const std::uint8_t* data, std::size_t size) { + if (size < 4) return false; for (std::size_t index = 0; index < size;) { - if (size - index < 4) throw std::runtime_error("Videodec2: truncated NAL length"); + if (size - index < 4) return false; const std::size_t length = (static_cast(data[index]) << 24u) | (static_cast(data[index + 1]) << 16u) | (static_cast(data[index + 2]) << 8u) | data[index + 3]; - if (length == 0 || length > size - index - 4) throw std::runtime_error("Videodec2: invalid NAL length"); - static constexpr std::uint8_t startCode[4] = {0, 0, 0, 1}; - decoder.annexB.insert(decoder.annexB.end(), startCode, startCode + 4); - decoder.annexB.insert(decoder.annexB.end(), data + index + 4, data + index + 4 + length); + if (length == 0 || length > size - index - 4) return false; index += 4 + length; } + return true; +} + +bool StartsWithStartCode(const std::uint8_t* data, std::size_t size) { + return size >= 3 && data[0] == 0 && data[1] == 0 && (data[2] == 1 || (size >= 4 && data[2] == 0 && data[3] == 1)); +} + +const std::vector* ToAnnexB(Decoder& decoder, const std::uint8_t* data, std::size_t size) { + const bool tiles = TilesAsAvcc(data, size); + const bool opensWithStartCode = StartsWithStartCode(data, size); + if (!tiles && !opensWithStartCode) throw std::runtime_error("Videodec2: access unit is neither Annex-B nor length-prefixed"); + const bool shortStartCode = opensWithStartCode && data[2] == 1; + bool lengthPrefixed = tiles; + if (tiles && opensWithStartCode) { + lengthPrefixed = shortStartCode ? !decoder.shortStartCodes : decoder.lengthPrefixed; + } else { + decoder.lengthPrefixed = tiles; + if (shortStartCode) decoder.shortStartCodes = true; + } + if (lengthPrefixed) { + decoder.annexB.clear(); + for (std::size_t index = 0; index < size;) { + const std::size_t length = (static_cast(data[index]) << 24u) | (static_cast(data[index + 1]) << 16u) | (static_cast(data[index + 2]) << 8u) | data[index + 3]; + static constexpr std::uint8_t startCode[4] = {0, 0, 0, 1}; + decoder.annexB.insert(decoder.annexB.end(), startCode, startCode + 4); + decoder.annexB.insert(decoder.annexB.end(), data + index + 4, data + index + 4 + length); + index += 4 + length; + } + return &decoder.annexB; + } + decoder.annexB.assign(data, data + size); return &decoder.annexB; } diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index bcc56dcb98..3ff904d406 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -21,6 +21,8 @@ constexpr std::int32_t PortOutDevice = 5; constexpr std::int32_t PortStateReady = 1; constexpr std::uint32_t VoiceFrameMs = 20; constexpr std::int32_t PortAttrVoiceActivity = 1001; +constexpr int VoiceErrorNotInitialized = static_cast(0x804E0801u); +constexpr int VoiceErrorAlreadyInitialized = static_cast(0x804E0802u); struct Port { std::int32_t type; @@ -66,7 +68,7 @@ int APS5_VABI sceVoiceInit(VoiceInitParam* param, int32_t version) { if (param == nullptr) APS5_INVALID_ARG_EX; auto& voice = State(); std::lock_guard lock(voice.mutex); - if (voice.initialized) Fail(__func__, "already initialized"); + if (voice.initialized) return VoiceErrorAlreadyInitialized; voice.initialized = true; return 0; } @@ -77,10 +79,10 @@ int APS5_VABI sceVoiceSetThreadsParams(void* params) { } int APS5_VABI sceVoiceStart(const VoiceStartParam* param) { - if (param == nullptr) APS5_INVALID_ARG_EX; auto& voice = State(); std::lock_guard lock(voice.mutex); - if (!voice.initialized) Fail(__func__, "library not initialized"); + if (!voice.initialized) return VoiceErrorNotInitialized; + if (param == nullptr) APS5_INVALID_ARG_EX; voice.started = true; return 0; } @@ -106,11 +108,11 @@ int APS5_VABI sceVoiceEnd_nid_postfix(void) { } int APS5_VABI sceVoiceCreatePort(uint32_t* port_id, const VoicePortParam* param) { - if (port_id == nullptr || param == nullptr) APS5_INVALID_ARG_EX; - if (param->port_type < PortInDevice || param->port_type > PortOutDevice) Fail(__func__, "unknown port type " + std::to_string(param->port_type)); auto& voice = State(); std::lock_guard lock(voice.mutex); - if (!voice.initialized) Fail(__func__, "library not initialized"); + if (!voice.initialized) return VoiceErrorNotInitialized; + if (port_id == nullptr || param == nullptr) APS5_INVALID_ARG_EX; + if (param->port_type < PortInDevice || param->port_type > PortOutDevice) Fail(__func__, "unknown port type " + std::to_string(param->port_type)); const bool voicePort = param->port_type == PortInVoice || param->port_type == PortOutVoice; const auto id = voice.nextId++; voice.ports.emplace(id, Port{param->port_type, param->volume, voicePort ? static_cast(param->voice.bitrate) : 0u}); @@ -171,9 +173,10 @@ int APS5_VABI sceVoiceGetVolume(uint32_t port_id, float* volume) { } int APS5_VABI sceVoiceGetPortInfo(uint32_t port_id, VoicePortInfo* info) { - if (info == nullptr) APS5_INVALID_ARG_EX; auto& voice = State(); std::lock_guard lock(voice.mutex); + if (!voice.initialized) return VoiceErrorNotInitialized; + if (info == nullptr) APS5_INVALID_ARG_EX; const auto& port = RequirePort(voice, port_id, __func__); *info = VoicePortInfo{}; info->port_type = port.type; @@ -255,6 +258,20 @@ int APS5_VABI sceVoiceResetPort(uint32_t port_id) { return 0; } +int APS5_VABI sceVoicePausePort(uint32_t port_id) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + RequirePort(voice, port_id, __func__); + return 0; +} + +int APS5_VABI sceVoiceResumePort(uint32_t port_id) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + RequirePort(voice, port_id, __func__); + return 0; +} + int APS5_VABI sceVoiceDisableChat(void) { auto& voice = State(); std::lock_guard lock(voice.mutex); diff --git a/core/libs/prx/libSceWebBrowserDialog/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index 2e3145513c..8ccf318370 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -87,4 +87,19 @@ int APS5_VABI sceWebBrowserDialogResetCookie() { return 0; } +int APS5_VABI sceWebBrowserDialogGetEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWebBrowserDialogNavigate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWebBrowserDialogSetZoom() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index db26a5c925..083cd31097 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -42,6 +42,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/VectorAlgorithms.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Math.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/MathExact.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Bessel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Random.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FloatBuiltins.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/HostEntropy.cpp @@ -313,6 +314,15 @@ if(BUILD_TESTING) set_tests_properties(guest_integer_builtins PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_stdio_variadic_tests ${LIBS_INCLUDE_DIR}/tests/GuestStdioVariadic.cpp) + target_include_directories(guest_stdio_variadic_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_stdio_variadic_tests PRIVATE libc) + configure_windows_unwind(guest_stdio_variadic_tests) + add_test(NAME guest_stdio_variadic COMMAND guest_stdio_variadic_tests) + if(WIN32) + set_tests_properties(guest_stdio_variadic PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_execute_once_tests ${LIBS_INCLUDE_DIR}/tests/GuestExecuteOnce.cpp) target_include_directories(guest_execute_once_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_execute_once_tests PRIVATE libc) @@ -342,6 +352,15 @@ if(BUILD_TESTING) set_tests_properties(guest_math_exact PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_bessel_tests ${LIBS_INCLUDE_DIR}/tests/GuestBessel.cpp) + target_include_directories(guest_bessel_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_bessel_tests PRIVATE libc) + configure_windows_unwind(guest_bessel_tests) + add_test(NAME guest_bessel COMMAND guest_bessel_tests) + if(WIN32) + set_tests_properties(guest_bessel PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_libc_threads_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibcThreads.cpp) target_include_directories(guest_libc_threads_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_libc_threads_tests PRIVATE libc libkernel) @@ -911,6 +930,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(guest_heap_trace_info_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestHeapTraceInfo.cpp) +target_include_directories(guest_heap_trace_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(guest_heap_trace_info_tests PRIVATE libc) +configure_windows_unwind(guest_heap_trace_info_tests) +if(BUILD_TESTING) + set_target_properties(guest_heap_trace_info_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME guest_heap_trace_info COMMAND guest_heap_trace_info_tests) + if(WIN32) + set_tests_properties(guest_heap_trace_info PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + if(MINGW) if(BUILD_TESTING) enable_language(ASM) diff --git a/core/libs/prx/libc/include/FilesystemError.hpp b/core/libs/prx/libc/include/FilesystemError.hpp index 37a4f7ea0e..bb50c8d70e 100644 --- a/core/libs/prx/libc/include/FilesystemError.hpp +++ b/core/libs/prx/libc/include/FilesystemError.hpp @@ -23,6 +23,20 @@ inline int FilesystemError(const std::error_code& error) { return 5; } +inline bool HasNonDirectoryPrefix_nid_no_patch(const std::filesystem::path& native) { + std::error_code error; + const auto trimmed = native.has_filename() ? native : native.parent_path(); + for (auto prefix = trimmed.parent_path(); prefix.has_relative_path(); prefix = prefix.parent_path()) { + const auto status = std::filesystem::status(prefix, error); + if (std::filesystem::exists(status)) return !std::filesystem::is_directory(status); + } + return false; +} + +inline int NotFoundError_nid_no_patch(const std::filesystem::path& native) { + return HasNonDirectoryPrefix_nid_no_patch(native) ? 20 : 2; +} + std::error_code RenameNative_nid_no_patch(const std::filesystem::path& source, const std::filesystem::path& destination, bool replacesExisting); inline int ResolvePathError_nid_no_patch(const char* path, std::filesystem::path& resolved, bool module = false, bool archived = false) { diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index 8669da3b08..91c50c7f61 100644 --- a/core/libs/prx/libc/include/GuestArena.hpp +++ b/core/libs/prx/libc/include/GuestArena.hpp @@ -8,6 +8,12 @@ // absolute address and breaks on host addresses outside that range. namespace GuestArena { +#ifdef ANYPS5_ENABLE_ASAN +inline constexpr std::uintptr_t GuestAddressOffset = 0x0000100000000000ull; +#else +inline constexpr std::uintptr_t GuestAddressOffset = 0; +#endif + #ifndef _WIN32 using SharedBackingResolver = bool (*)(std::uintptr_t address, std::size_t bytes, int* file, std::uint64_t* offset); using SharedBackingWriter = bool (*)(std::uintptr_t address, const void* source, std::size_t bytes); diff --git a/core/libs/prx/libc/include/GuestDirectory.hpp b/core/libs/prx/libc/include/GuestDirectory.hpp index 092289d03a..3f83ab1a18 100644 --- a/core/libs/prx/libc/include/GuestDirectory.hpp +++ b/core/libs/prx/libc/include/GuestDirectory.hpp @@ -14,4 +14,4 @@ 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)); +extern "C" void* APS5_VABI OpenDescriptorDirectory_nid_no_patch(const char* hostPath, int descriptor, int (APS5_VABI* closeDescriptor)(int), std::uint64_t position); diff --git a/core/libs/prx/libc/include/RenameDot.hpp b/core/libs/prx/libc/include/RenameDot.hpp new file mode 100644 index 0000000000..a43a778412 --- /dev/null +++ b/core/libs/prx/libc/include/RenameDot.hpp @@ -0,0 +1,26 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_RENAMEDOT_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_RENAMEDOT_HPP + +#include +#include +#include "FilesystemError.hpp" + +inline int RenameDotError_nid_no_patch(const char* path) { + std::size_t length = std::char_traits::length(path); + const auto separator = [](char character) { return character == '/' || character == '\\'; }; + while (length > 1 && separator(path[length - 1])) --length; + std::size_t start = length; + while (start > 0 && !separator(path[start - 1])) --start; + const std::size_t name = length - start; + const bool dot = name == 1 && path[start] == '.'; + const bool dotDot = name == 2 && path[start] == '.' && path[start + 1] == '.'; + if (!dot && !dotDot) return 0; + const std::string parent = start == 0 ? std::string(".") : std::string(path, start); + std::filesystem::path resolved; + if (const int error = ResolvePathError_nid_no_patch(parent.c_str(), resolved)) return error; + std::error_code status; + if (!std::filesystem::exists(resolved, status)) return 2; + return std::filesystem::is_directory(resolved, status) ? 22 : 20; +} + +#endif diff --git a/core/libs/prx/libc/include/SonyCrt.hpp b/core/libs/prx/libc/include/SonyCrt.hpp index 2790a06d04..cf1879e2c0 100644 --- a/core/libs/prx/libc/include/SonyCrt.hpp +++ b/core/libs/prx/libc/include/SonyCrt.hpp @@ -88,21 +88,39 @@ inline int SonyFormatAsctime(char* buffer, std::size_t size, const GuestTm& time static const char* const days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; static const char* const months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; if (!buffer || size < 26) return EINVAL; - if (time.tm_wday < 0 || time.tm_wday > 6 || time.tm_mon < 0 || time.tm_mon > 11) return EINVAL; + const int year = 1900 + time.tm_year; + if (time.tm_wday < 0 || time.tm_wday > 6 || time.tm_mon < 0 || time.tm_mon > 11 || + time.tm_mday < 1 || time.tm_mday > 31 || time.tm_hour < 0 || time.tm_hour > 23 || + time.tm_min < 0 || time.tm_min > 59 || time.tm_sec < 0 || time.tm_sec > 60 || + year < 0 || year > 9999) { + buffer[0] = '\0'; + return EINVAL; + } std::snprintf(buffer, size, "%.3s %.3s %2d %.2d:%.2d:%.2d %4d\n", days[time.tm_wday], months[time.tm_mon], - time.tm_mday, time.tm_hour, time.tm_min, time.tm_sec, 1900 + time.tm_year); + time.tm_mday, time.tm_hour, time.tm_min, time.tm_sec, year); return 0; } inline int SonyCtimeS(char* buffer, std::size_t size, const std::int64_t* time) { - if (!buffer || size < 26 || !time) return EINVAL; + if (!buffer || size < 26) return EINVAL; + if (!time) { + buffer[0] = '\0'; + return EINVAL; + } GuestTm parts{}; - if (!localtime_s_nid_postfix(time, &parts)) return EINVAL; + if (!localtime_s_nid_postfix(time, &parts)) { + buffer[0] = '\0'; + return EINVAL; + } return SonyFormatAsctime(buffer, size, parts); } inline int SonyAsctimeS(char* buffer, std::size_t size, const GuestTm* time) { - if (!buffer || size < 26 || !time) return EINVAL; + if (!buffer || size < 26) return EINVAL; + if (!time) { + buffer[0] = '\0'; + return EINVAL; + } return SonyFormatAsctime(buffer, size, *time); } diff --git a/core/libs/prx/libc/src/Bessel.cpp b/core/libs/prx/libc/src/Bessel.cpp new file mode 100644 index 0000000000..7ba3585ad3 --- /dev/null +++ b/core/libs/prx/libc/src/Bessel.cpp @@ -0,0 +1,424 @@ +#include +#include +#include +#include +#include +#include + +#include "prx/libc/include/general/VabiMacros.hpp" + +namespace { + +constexpr double One = 1.0; +constexpr double Huge = 1.0e300; +constexpr double InverseSqrtPi = 5.64189583547756279280e-01; +constexpr double TwoOverPi = 6.36619772367581382433e-01; +constexpr double LogMax = 7.09782712893383973096e+02; + +constexpr double PZeroNumerators[4][6] = { + {0.00000000000000000000e+00, -7.03124999999900357484e-02, -8.08167041275349795626e+00, -2.57063105679704847262e+02, -2.48521641009428822144e+03, -5.25304380490729545272e+03}, + {-1.14125464691894502584e-11, -7.03124940873599280078e-02, -4.15961064470587782438e+00, -6.76747652265167261021e+01, -3.31231299649172967747e+02, -3.46433388365604912451e+02}, + {-2.54704601771951915620e-09, -7.03119616381481654654e-02, -2.40903221549529611423e+00, -2.19659774734883086467e+01, -5.80791704701737572236e+01, -3.14479470594888503854e+01}, + {-8.87534333032526411254e-08, -7.03030995483624743247e-02, -1.45073846780952986357e+00, -7.63569613823527770791e+00, -1.11931668860356747786e+01, -3.23364579351335335033e+00}, +}; +constexpr double PZeroDenominators[4][5] = { + {1.16534364619668181717e+02, 3.83374475364121826715e+03, 4.05978572648472545552e+04, 1.16752972564375915681e+05, 4.76277284146730962675e+04}, + {6.07539382692300335975e+01, 1.05125230595704579173e+03, 5.97897094333855784498e+03, 9.62544514357774460223e+03, 2.40605815922939109441e+03}, + {3.58560338055209726349e+01, 3.61513983050303863820e+02, 1.19360783792111533330e+03, 1.12799679856907414432e+03, 1.73580930813335754692e+02}, + {2.22202997532088808441e+01, 1.36206794218215208048e+02, 2.70470278658083486789e+02, 1.53875394208320329881e+02, 1.46576176948256193810e+01}, +}; +constexpr double QZeroNumerators[4][6] = { + {0.00000000000000000000e+00, 7.32421874999935051953e-02, 1.17682064682252693899e+01, 5.57673380256401856059e+02, 8.85919720756468632317e+03, 3.70146267776887834771e+04}, + {1.84085963594515531381e-11, 7.32421766612684765896e-02, 5.83563508962056953777e+00, 1.35111577286449829671e+02, 1.02724376596164097464e+03, 1.98997785864605384631e+03}, + {4.37741014089738620906e-09, 7.32411180042911447163e-02, 3.34423137516170720929e+00, 4.26218440745412650017e+01, 1.70808091340565596283e+02, 1.66733948696651168575e+02}, + {1.50444444886983272379e-07, 7.32234265963079278272e-02, 1.99819174093815998816e+00, 1.44956029347885735348e+01, 3.16662317504781540833e+01, 1.62527075710929267416e+01}, +}; +constexpr double QZeroDenominators[4][6] = { + {1.63776026895689824414e+02, 8.09834494656449805916e+03, 1.42538291419120476348e+05, 8.03309257119514397345e+05, 8.40501579819060512818e+05, -3.43899293537866615225e+05}, + {8.27766102236537761883e+01, 2.07781416421392987104e+03, 1.88472887785718085070e+04, 5.67511122894947329769e+04, 3.59767538425114471465e+04, -5.35434275601944773371e+03}, + {4.87588729724587182091e+01, 7.09689221056606015736e+02, 3.70414822620111362994e+03, 6.46042516752568917582e+03, 2.51633368920368957333e+03, -1.49247451836156386662e+02}, + {3.03655848355219184498e+01, 2.69348118608049844624e+02, 8.44783757595320139444e+02, 8.82935845112488550512e+02, 2.12666388511798828631e+02, -5.31095493882666946917e+00}, +}; +constexpr double POneNumerators[4][6] = { + {0.00000000000000000000e+00, 1.17187499999988647970e-01, 1.32394806593073575129e+01, 4.12051854307378562225e+02, 3.87474538913960532227e+03, 7.91447954031891731574e+03}, + {1.31990519556243522749e-11, 1.17187493190614097638e-01, 6.80275127868432871736e+00, 1.08308182990189109773e+02, 5.17636139533199752805e+02, 5.28715201363337541807e+02}, + {3.02503916137373618024e-09, 1.17186865567253592491e-01, 3.93297750033315640650e+00, 3.51194035591636932736e+01, 9.10550110750781271918e+01, 4.85590685197364919645e+01}, + {1.07710830106873743082e-07, 1.17176219462683348094e-01, 2.36851496667608785174e+00, 1.22426109148261232917e+01, 1.76939711271687727390e+01, 5.07352312588818499250e+00}, +}; +constexpr double POneDenominators[4][5] = { + {1.14207370375678408436e+02, 3.65093083420853463394e+03, 3.69562060269033463555e+04, 9.76027935934950801311e+04, 3.08042720627888811578e+04}, + {5.92805987221131331921e+01, 9.91401418733614377743e+02, 5.35326695291487976647e+03, 7.84469031749551231769e+03, 1.50404688810361062679e+03}, + {3.47913095001251519989e+01, 3.36762458747825746741e+02, 1.04687139975775130551e+03, 8.90811346398256432622e+02, 1.03787932439639277504e+02}, + {2.14364859363821409488e+01, 1.25290227168402751090e+02, 2.32276469057162813669e+02, 1.17679373287147100768e+02, 8.36463893371618283368e+00}, +}; +constexpr double QOneNumerators[4][6] = { + {0.00000000000000000000e+00, -1.02539062499992714161e-01, -1.62717534544589987888e+01, -7.59601722513950107896e+02, -1.18498066702429587167e+04, -4.84385124285750353010e+04}, + {-2.08979931141764104297e-11, -1.02539050241375426231e-01, -8.05644828123936029840e+00, -1.83669607474888380239e+02, -1.37319376065508163265e+03, -2.61244440453215656817e+03}, + {-5.07831226461766561369e-09, -1.02537829820837089745e-01, -4.61011581139473403113e+00, -5.78472216562783643212e+01, -2.28244540737631695038e+02, -2.19210128478909325622e+02}, + {-1.78381727510958865572e-07, -1.02517042607985553460e-01, -2.75220568278187460720e+00, -1.96636162643703720221e+01, -4.23253133372830490089e+01, -2.13719211703704061733e+01}, +}; +constexpr double QOneDenominators[4][6] = { + {1.61395369700722909556e+02, 7.82538599923348465381e+03, 1.33875336287249578163e+05, 7.19657723683240939863e+05, 6.66601232617776375264e+05, -2.94490264303834643215e+05}, + {8.12765501384335777857e+01, 1.99179873460485964642e+03, 1.74684851924908907677e+04, 4.98514270910352279316e+04, 2.79480751638918118260e+04, -4.71918354795128470869e+03}, + {4.76651550323729509273e+01, 6.73865112676699709482e+02, 3.38015286679526343505e+03, 5.54772909720722782367e+03, 1.90311919338810798763e+03, -1.35201191444307340817e+02}, + {2.95333629060523854548e+01, 2.52981549982190529136e+02, 7.57502834868645436472e+02, 7.39393205320467245656e+02, 1.55949003336666123687e+02, -4.95949898822628210127e+00}, +}; + +constexpr double J0Numerator[] = {1.56249999999999947958e-02, -1.89979294238854721751e-04, 1.82954049532700665670e-06, -4.61832688532103189199e-09}; +constexpr double J0Denominator[] = {1.56191029464890010492e-02, 1.16926784663337450260e-04, 5.13546550207318111446e-07, 1.16614003333790000205e-09}; +constexpr double Y0Numerator[] = { + -7.38042951086872317523e-02, 1.76666452509181115538e-01, -1.38185671945596898896e-02, 3.47453432093683650238e-04, + -3.81407053724364161125e-06, 1.95590137035022920206e-08, -3.98205194132103398453e-11, +}; +constexpr double Y0Denominator[] = {1.27304834834123699328e-02, 7.60068627350353253702e-05, 2.59150851840457805467e-07, 4.41110311332675467403e-10}; +constexpr double J1Numerator[] = {-6.25000000000000000000e-02, 1.40705666955189706048e-03, -1.59955631084035597520e-05, 4.96727999609584448412e-08}; +constexpr double J1Denominator[] = { + 1.91537599538363460805e-02, 1.85946785588630915560e-04, 1.17718464042623683263e-06, 5.04636257076217042715e-09, 1.23542274426137913908e-11, +}; +constexpr double Y1Numerator[] = { + -1.96057090646238940668e-01, 5.04438716639811282616e-02, -1.91256895875763547298e-03, 2.35252600561610495928e-05, -9.19099158039878874504e-08, +}; +constexpr double Y1Denominator[] = { + 1.99167318236649903973e-02, 2.02552581025135171496e-04, 1.35608801097516229404e-06, 6.22741452364621501295e-09, 1.66559246207992079114e-11, +}; + +std::uint32_t HighWord(double x) { + return static_cast(std::bit_cast(x) >> 32); +} + +std::uint32_t LowWord(double x) { + return static_cast(std::bit_cast(x)); +} + +double SecondKindAtZero() { + return -std::numeric_limits::infinity(); +} + +double SecondKindOfNegative() { + return std::bit_cast(0x7ff8000000000000ull); +} + +double Signed(std::uint32_t value) { + return static_cast(static_cast(value)); +} + +double SquareRoot(double x) { + return _mm_cvtsd_f64(_mm_sqrt_sd(_mm_setzero_pd(), _mm_set_sd(x))); +} + +const bool HardwareFma = [] { + __builtin_cpu_init(); + return __builtin_cpu_supports("fma") != 0; +}(); + +[[gnu::target("fma")]] double HardwareFused(double a, double b, double c) { + return _mm_cvtsd_f64(_mm_fmadd_sd(_mm_set_sd(a), _mm_set_sd(b), _mm_set_sd(c))); +} + +double Fused(double a, double b, double c) { + return HardwareFma ? HardwareFused(a, b, c) : std::fma(a, b, c); +} + +template +double Horner(double z, const double (&coefficients)[Count]) { + double result = coefficients[Count - 1]; + for (std::size_t index = Count - 1; index-- > 0;) result = Fused(z, result, coefficients[index]); + return result; +} + +std::size_t AsymptoticRange(std::uint32_t magnitude) { + if (magnitude >= 0x40200000u) return 0; + if (magnitude >= 0x40122e8bu) return 1; + if (magnitude >= 0x4006db6du) return 2; + return 3; +} + +template +double Asymptotic(double x, const double (&numerator)[NumeratorCount], const double (&denominator)[DenominatorCount]) { + const double z = One / (x * x); + return Horner(z, numerator) / Fused(z, Horner(z, denominator), One); +} + +double PZero(double x, std::uint32_t magnitude) { + const std::size_t range = AsymptoticRange(magnitude); + return Asymptotic(x, PZeroNumerators[range], PZeroDenominators[range]) + One; +} + +double QZero(double x, std::uint32_t magnitude) { + const std::size_t range = AsymptoticRange(magnitude); + return (Asymptotic(x, QZeroNumerators[range], QZeroDenominators[range]) + -0.125) / x; +} + +double POne(double x, std::uint32_t magnitude) { + const std::size_t range = AsymptoticRange(magnitude); + return Asymptotic(x, POneNumerators[range], POneDenominators[range]) + One; +} + +double QOne(double x, std::uint32_t magnitude) { + const std::size_t range = AsymptoticRange(magnitude); + return (Asymptotic(x, QOneNumerators[range], QOneDenominators[range]) + 0.375) / x; +} + +} + +extern "C" { + +double APS5_VABI j0_nid_postfix(double x) { + const std::uint32_t magnitude = HighWord(x) & 0x7fffffffu; + if (magnitude >= 0x7ff00000u) return One / (x * x); + const double absolute = std::fabs(x); + if (magnitude >= 0x40000000u) { + const double s = std::sin(absolute); + const double c = std::cos(x); + double ss = s - c; + double cc = c + s; + if (magnitude < 0x7fe00000u) { + const double z = -std::cos(absolute + absolute); + if (s * c < 0.0) cc = z / ss; + else ss = z / cc; + } + if (magnitude > 0x48000000u) return cc * InverseSqrtPi / SquareRoot(absolute); + const double u = PZero(absolute, magnitude); + const double v = QZero(absolute, magnitude); + return Fused(u, cc, -(ss * v)) * InverseSqrtPi / SquareRoot(absolute); + } + if (Huge + absolute > One && magnitude < 0x3f200000u) { + if (magnitude < 0x3e400000u) return One; + return Fused(x * x, -0.25, One); + } + const double z = x * x; + const double r = z * Horner(z, J0Numerator); + const double s = Fused(z, Horner(z, J0Denominator), One); + if (magnitude < 0x3ff00000u) return Fused(z, r / s + -0.25, One); + const double u = absolute * 0.5; + return Fused(One + u, One - u, z * (r / s)); +} + +double APS5_VABI y0_nid_postfix(double x) { + const std::uint32_t high = HighWord(x); + const std::uint32_t magnitude = high & 0x7fffffffu; + if (magnitude >= 0x7ff00000u) return One / Fused(x, x, x); + if ((magnitude | LowWord(x)) == 0) return SecondKindAtZero(); + if ((high & 0x80000000u) != 0) return SecondKindOfNegative(); + if (magnitude >= 0x40000000u) { + const double s = std::sin(x); + const double c = std::cos(x); + double ss = s - c; + double cc = s + c; + if (magnitude < 0x7fe00000u) { + const double z = -std::cos(x + x); + if (s * c < 0.0) cc = z / ss; + else ss = z / cc; + } + if (magnitude > 0x48000000u) return ss * InverseSqrtPi / SquareRoot(x); + const double u = PZero(x, magnitude); + const double v = QZero(x, magnitude); + return Fused(u, ss, cc * v) * InverseSqrtPi / SquareRoot(x); + } + if (magnitude <= 0x3e400000u) return Fused(TwoOverPi, std::log(x), Y0Numerator[0]); + const double z = x * x; + const double u = Horner(z, Y0Numerator); + const double v = Fused(z, Horner(z, Y0Denominator), One); + return Fused(std::log(x) * j0_nid_postfix(x), TwoOverPi, u / v); +} + +double APS5_VABI j1_nid_postfix(double x) { + const std::uint32_t high = HighWord(x); + const std::uint32_t magnitude = high & 0x7fffffffu; + if (magnitude >= 0x7ff00000u) return One / x; + if (magnitude >= 0x40000000u) { + const double y = std::fabs(x); + const double s = std::sin(y); + const double c = std::cos(x); + double ss = -s - c; + double cc = s - c; + if (magnitude < 0x7fe00000u) { + const double z = std::cos(y + y); + if (s * c > 0.0) cc = z / ss; + else ss = z / cc; + } + double result; + if (magnitude > 0x48000000u) { + result = cc * InverseSqrtPi / SquareRoot(y); + } else { + const double u = POne(y, magnitude); + const double v = QOne(y, magnitude); + result = Fused(u, cc, -(ss * v)) * InverseSqrtPi / SquareRoot(y); + } + return (high & 0x80000000u) != 0 ? -result : result; + } + if (Huge + x > One && magnitude < 0x3e400000u) return x * 0.5; + const double z = x * x; + const double r = z * Horner(z, J1Numerator); + const double s = Fused(z, Horner(z, J1Denominator), One); + return Fused(x, 0.5, r * x / s); +} + +double APS5_VABI y1_nid_postfix(double x) { + const std::uint32_t high = HighWord(x); + const std::uint32_t magnitude = high & 0x7fffffffu; + if (magnitude >= 0x7ff00000u) return One / Fused(x, x, x); + if ((magnitude | LowWord(x)) == 0) return SecondKindAtZero(); + if ((high & 0x80000000u) != 0) return SecondKindOfNegative(); + if (magnitude >= 0x40000000u) { + const double s = std::sin(x); + const double c = std::cos(x); + double ss = -s - c; + double cc = s - c; + if (magnitude < 0x7fe00000u) { + const double z = std::cos(x + x); + if (s * c > 0.0) cc = z / ss; + else ss = z / cc; + } + if (magnitude > 0x48000000u) return ss * InverseSqrtPi / SquareRoot(x); + const double u = POne(x, magnitude); + const double v = QOne(x, magnitude); + return Fused(u, ss, cc * v) * InverseSqrtPi / SquareRoot(x); + } + if (magnitude <= 0x3c900000u) return -TwoOverPi / x; + const double z = x * x; + const double u = Horner(z, Y1Numerator); + const double v = Fused(z, Horner(z, Y1Denominator), One); + return Fused(x, u / v, Fused(std::log(x), j1_nid_postfix(x), -One / x) * TwoOverPi); +} + +double APS5_VABI jn_nid_postfix(int order, double x) { + const std::uint32_t high = HighWord(x); + const std::uint32_t low = LowWord(x); + const std::uint32_t magnitude = high & 0x7fffffffu; + if ((magnitude | (low != 0 ? 1u : 0u)) > 0x7ff00000u) return x + x; + const double argument = order < 0 ? -x : x; + const std::uint32_t n = order < 0 ? 0u - static_cast(order) : static_cast(order); + if (n == 1) return j1_nid_postfix(argument); + if (n == 0) return j0_nid_postfix(argument); + const bool negative = ((high ^ static_cast(order)) & 0x80000000u) != 0; + double result = 0.0; + if (magnitude < 0x7ff00000u && (magnitude | low) != 0) { + const double absolute = std::fabs(x); + const double orderValue = Signed(n); + if (absolute >= orderValue) { + if (magnitude >= 0x52d00000u) { + double sum; + switch (n & 3u) { + case 0: sum = std::sin(absolute) + std::cos(argument); break; + case 1: sum = std::sin(absolute) - std::cos(argument); break; + case 2: sum = -std::cos(argument) - std::sin(absolute); break; + default: sum = std::cos(argument) - std::sin(absolute); break; + } + result = sum * InverseSqrtPi / SquareRoot(absolute); + } else { + double previous = j0_nid_postfix(absolute); + result = j1_nid_postfix(absolute); + std::uint32_t twice = 2; + for (std::uint32_t remaining = n - 1; remaining != 0; --remaining) { + const double next = Fused(result, Signed(twice) / absolute, -previous); + previous = result; + result = next; + twice += 2; + } + } + } else if (magnitude < 0x3e100000u) { + if (n <= 33u) { + const double half = absolute * 0.5; + double power = half; + double factorial = One; + for (std::uint32_t index = 2; index <= n; ++index) { + power = half * power; + factorial *= Signed(index); + } + result = power / factorial; + } + } else { + const double h = 2.0 / absolute; + const double w = Signed(2u * n) / absolute; + double q0 = w; + double z = h + w; + double q1 = Fused(w, z, -One); + std::uint32_t k = 1; + while (q1 < 1.0e9) { + z = h + z; + ++k; + const double next = Fused(z, q1, -q0); + q0 = q1; + q1 = next; + } + const std::int32_t lowest = static_cast(2u * n); + std::uint32_t index = 2u * (k + n); + double t = 0.0; + do { + t = One / (Signed(index) / absolute - t); + index -= 2u; + } while (static_cast(index) >= lowest); + const double bound = std::log(std::fabs(h * orderValue)) * orderValue; + double a = t; + double b = One; + double di = Signed(2u * n - 2u); + std::int32_t remaining = static_cast(n); + const bool rescale = !(bound < LogMax); + do { + const double next = b * di / absolute - a; + di += -2.0; + a = b; + b = next; + if (rescale && b > 1.0e100) { + t /= b; + a /= b; + b = One; + } + --remaining; + } while (remaining > 1); + const double z0 = j0_nid_postfix(absolute); + const double z1 = j1_nid_postfix(absolute); + result = std::fabs(z0) >= std::fabs(z1) ? z0 * t / b : t * z1 / a; + } + } + const double flipped = negative ? -result : result; + return (n & 1u) != 0 ? flipped : result; +} + +double APS5_VABI yn_nid_postfix(int order, double x) { + const std::uint32_t high = HighWord(x); + const std::uint32_t low = LowWord(x); + const std::uint32_t magnitude = high & 0x7fffffffu; + if ((magnitude | (low != 0 ? 1u : 0u)) > 0x7ff00000u) return x + x; + if ((magnitude | low) == 0) return SecondKindAtZero(); + if ((high & 0x80000000u) != 0) return SecondKindOfNegative(); + std::uint32_t n = static_cast(order); + std::int32_t sign = 1; + if (order < 0) { + n = 0u - n; + sign = 1 - static_cast((n << 1) & 2u); + } + if (n == 1) return static_cast(sign) * y1_nid_postfix(x); + if (n == 0) return y0_nid_postfix(x); + if (magnitude == 0x7ff00000u) return 0.0; + double result; + if (magnitude >= 0x52d00000u) { + const double s = std::sin(x); + const double c = std::cos(x); + double sum; + switch (n & 3u) { + case 0: sum = s - c; break; + case 1: sum = -s - c; break; + case 2: sum = c - s; break; + default: sum = s + c; break; + } + result = sum * InverseSqrtPi / SquareRoot(x); + } else { + double previous = y0_nid_postfix(x); + result = y1_nid_postfix(x); + std::uint32_t twice = 2; + std::int32_t index = 2; + while (HighWord(result) != 0xfff00000u) { + const double next = Fused(result, Signed(twice) / x, -previous); + previous = result; + result = next; + if (index >= static_cast(n)) break; + ++index; + twice += 2; + } + } + return sign > 0 ? result : -result; +} + +} diff --git a/core/libs/prx/libc/src/Directory.cpp b/core/libs/prx/libc/src/Directory.cpp index f86c502cfe..2ea8cc0946 100644 --- a/core/libs/prx/libc/src/Directory.cpp +++ b/core/libs/prx/libc/src/Directory.cpp @@ -3,6 +3,7 @@ #include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/AppArchive.hpp" #include +#include #include #include #include @@ -116,7 +117,11 @@ void* APS5_VABI opendir_nid_postfix(const char* path) { 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); return nullptr; } + 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; } @@ -178,12 +183,26 @@ void APS5_VABI seekdir_nid_postfix(void* handle, std::int64_t location) { SeekNative(directory.native, static_cast(location)); } -void* APS5_VABI OpenDescriptorDirectory_nid_no_patch(const char* hostPath, int descriptor, int (APS5_VABI* closeDescriptor)(int)) { +void* APS5_VABI OpenDescriptorDirectory_nid_no_patch(const char* hostPath, int descriptor, int (APS5_VABI* closeDescriptor)(int), std::uint64_t position) { try { auto directory = std::make_unique(); directory->path = std::filesystem::path(reinterpret_cast(hostPath)); +#ifdef _WIN32 directory->native = OpenNative(directory->path); if (!directory->native) { errno = DirectoryError(errno); return nullptr; } + std::filesystem::path component; + std::string name; + std::uint64_t inode = 0; + for (std::uint64_t skipped = 0; skipped < position; ++skipped) { + if (!ReadNative(directory->native, component, name, inode)) break; + } +#else + (void)position; + const int duplicate = ::dup(descriptor); + if (duplicate < 0) { errno = DirectoryError(errno); return nullptr; } + directory->native = ::fdopendir(duplicate); + if (!directory->native) { const int error = errno; ::close(duplicate); errno = DirectoryError(error); return nullptr; } +#endif directory->descriptor = descriptor; directory->closeDescriptor = closeDescriptor; return directory.release(); diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 3ad3615c15..4b8f5328ba 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -301,6 +301,12 @@ FileStream* APS5_VABI fdopen_nid_postfix(int descriptor, const char* mode) { } catch (const std::bad_alloc&) { errno = 12; return nullptr; } } +static int OpenFailure(const std::filesystem::path& path, bool writes, int hostError) { + const int error = GuestErrnoFromHost_nid_no_patch(hostError); + std::error_code status; + return writes && error == 13 && std::filesystem::is_directory(path, status) ? 21 : error; +} + FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode, FileStream* stream) { if (!stream || !mode) { errno = 22; return nullptr; } if (!filename) { errno = 45; return nullptr; } // Mode-only reopening is not supported. @@ -331,7 +337,8 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode if (!path.empty() && parsed->writes) RecordWrittenPath_nid_no_patch(path); return stream; } - const int error = GuestErrnoFromHost_nid_no_patch(errno); + int error = OpenFailure(path, parsed->writes, errno); + if (error == 2) error = NotFoundError_nid_no_patch(path); if (stream->IsDynamic()) delete stream; errno = error; return nullptr; @@ -363,7 +370,8 @@ FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) if (const int error = ResolvePathError_nid_no_patch(filename, fpath)) { errno = error; return nullptr; } std::unique_ptr handle(OpenNative(fpath, parsed->native), std::fclose); if (!handle) { - errno = GuestErrnoFromHost_nid_no_patch(errno); + errno = OpenFailure(fpath, parsed->writes, errno); + if (errno == 2) errno = NotFoundError_nid_no_patch(fpath); return nullptr; } // APS5_LOG_OUT("success: \"%s\"", abs_path.c_str()); diff --git a/core/libs/prx/libc/src/Filesystem.cpp b/core/libs/prx/libc/src/Filesystem.cpp index cff29e87ec..510477eb7a 100644 --- a/core/libs/prx/libc/src/Filesystem.cpp +++ b/core/libs/prx/libc/src/Filesystem.cpp @@ -12,8 +12,8 @@ #endif extern "C" int APS5_VABI access_nid_postfix(const char* path, int mode) { - if (!path) { errno = 14; return -1; } if (mode < 0 || (mode & ~7)) { errno = 22; return -1; } + if (!path) { errno = 14; return -1; } if (!*path) { errno = 2; return -1; } try { const auto resolved = ResolvePath_nid_no_patch(path); @@ -21,6 +21,7 @@ extern "C" int APS5_VABI access_nid_postfix(const char* path, int mode) { const DWORD attributes = GetFileAttributesW(resolved.c_str()); if (attributes == INVALID_FILE_ATTRIBUTES) { errno = FilesystemError(std::error_code(static_cast(GetLastError()), std::system_category())); + if (errno == 2) errno = NotFoundError_nid_no_patch(resolved); return -1; } if (mode == 0) return 0; @@ -110,6 +111,7 @@ extern "C" int APS5_VABI remove_nid_postfix(const char* path) { #endif if (error || !removed) { errno = error ? FilesystemError(error) : 2; + if (errno == 2) errno = NotFoundError_nid_no_patch(resolved); return -1; } RecordWrittenPath_nid_no_patch(resolved); diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 0d883aa904..e78be1d4a6 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -565,4 +565,43 @@ int APS5_VABI puts_nid_postfix(const char* s) { return std::puts(s); } +extern FileStream* __stdinp_nid_postfix; + +int APS5_VABI vfscanf_nid_postfix(FileStream* stream, const char* format, VaList* args) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } + auto* native = GetNativeStream(stream); +#ifdef _WIN32 + const int result = LibcDetail::ScanFileWindows_nid_no_patch(native, format, args); +#else + RejectGuestWideScan(format); + std::va_list copy; + va_copy(copy, *reinterpret_cast(args)); + const int result = HostVfscanf(native, format, copy); + va_end(copy); +#endif + stream->SyncStatus(); + return result; +} + +int APS5_VABI vscanf_nid_postfix(const char* format, VaList* args) { + return vfscanf_nid_postfix(__stdinp_nid_postfix, format, args); +} + +int APS5_VABI scanf_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_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 713a520591..1526f39fef 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -333,6 +333,32 @@ int APS5_VABI wprintf_nid_postfix(const char16_t* format, ...) { return result; } +int APS5_VABI vfwprintf_nid_postfix(FileStream* stream, const char16_t* format, VaList* args) { + if (stream == nullptr || format == nullptr) { errno = 22; return -1; } + if (!stream->IsOpen()) { errno = 9; return -1; } + int result = -1; + try { + const std::u16string text = FormatWide(format, args); + const std::string bytes = ToUtf8(text); + if (std::fwrite(bytes.data(), 1, bytes.size(), GetNativeStream(stream)) == bytes.size()) result = static_cast(text.size()); + } catch (const std::exception&) { + errno = 22; + } + stream->SyncStatus(); + return result; +} + +int APS5_VABI fwprintf_nid_postfix(FileStream* stream, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfwprintf_nid_postfix(stream, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} + +int APS5_VABI vwprintf_nid_postfix(const char16_t* format, VaList* args) { + return vfwprintf_nid_postfix(__stdoutp_nid_postfix, format, args); +} + int APS5_VABI fputwc_nid_postfix(char16_t value, FileStream* stream) { if (stream == nullptr) { errno = 22; return -1; } try { diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 67444c12b6..7b86b2c269 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -31,6 +31,7 @@ #include #endif #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/AppArchive.hpp" #include "prx/libc/include/ApplicationHeap.hpp" @@ -246,6 +247,20 @@ std::vector WithTrailingSeparator(std::vector names, b } std::filesystem::path Resolve(WorkingDirectory& state, const char* path, bool module = false, bool archived = false) { std::string text(path); + constexpr std::size_t MaxPath = 1024; + constexpr std::size_t MaxName = 255; + if (text.size() >= MaxPath) throw std::filesystem::filesystem_error("Guest path is too long", std::make_error_code(std::errc::filename_too_long)); + std::size_t name = 0; + for (std::size_t index = 0; index < text.size(); ++index) { + name = text[index] == '/' || text[index] == '\\' ? 0 : name + 1; + if (name <= MaxName) continue; + const std::size_t end = index + 1 - name; + std::error_code status; + if (end == 0 || std::filesystem::is_directory(Resolve(state, text.substr(0, end).c_str()), status)) { + throw std::filesystem::filesystem_error("Guest path name is too long", std::make_error_code(std::errc::filename_too_long)); + } + break; + } for (auto& character : text) if (character == '\\') character = '/'; #ifdef _WIN32 // Preserve the existing ability to pass explicit native drive paths. @@ -584,7 +599,7 @@ extern "C" int APS5_VABI chdir_nid_postfix(const char* path) { const auto resolved = Resolve(state, path, false, true); AppArchiveRefuseOnly_nid_no_patch(resolved, path, "changing the working directory to"); const int error = SetWorkingDirectory(state, resolved); - if (error) { errno = error; return -1; } + if (error) { errno = error == 2 ? NotFoundError_nid_no_patch(resolved) : error; return -1; } return 0; } catch (const std::bad_alloc&) { errno = 12; return -1; } catch (const std::filesystem::filesystem_error& error) { errno = DirectoryFailure(error.code()); return -1; } diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 8b0fc757e8..1fd9510565 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -44,11 +44,7 @@ void noteAccess(const void* pointer, std::size_t bytes) { if (const auto callback = accessObserver.load(std::memory_order_acquire)) callback(reinterpret_cast(pointer), bytes); } -#ifdef ANYPS5_ENABLE_ASAN -constexpr std::uintptr_t GuestArenaOffset = 0x0000100000000000ull; -#else -constexpr std::uintptr_t GuestArenaOffset = 0; -#endif +constexpr std::uintptr_t GuestArenaOffset = GuestArena::GuestAddressOffset; constexpr std::uintptr_t ArenaStart = 0x0000000200000000ull + GuestArenaOffset; constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull + GuestArenaOffset; constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull + GuestArenaOffset; diff --git a/core/libs/prx/libc/src/GuestWriteWatch.cpp b/core/libs/prx/libc/src/GuestWriteWatch.cpp index 52907e1cb0..e0a4344e67 100644 --- a/core/libs/prx/libc/src/GuestWriteWatch.cpp +++ b/core/libs/prx/libc/src/GuestWriteWatch.cpp @@ -29,7 +29,7 @@ #include #include #include -#include +#include #include #include #endif @@ -484,20 +484,41 @@ private: std::vector pages; }; - struct Turns { - std::atomic next{0}; - std::atomic serving{0}; + struct Mutex { + Mutex() { + pthread_mutexattr_t attributes; + pthread_mutexattr_init(&attributes); + pthread_mutexattr_setpolicy_np(&attributes, PTHREAD_MUTEX_POLICY_FIRSTFIT_NP); + pthread_mutex_init(&mutex, &attributes); + pthread_mutexattr_destroy(&attributes); + pthread_cond_init(&idle, nullptr); + } + pthread_mutex_t mutex; + pthread_cond_t idle; + std::atomic faulting{0}; }; struct Lock { - explicit Lock(Turns& turns) : _turns(turns), _turn(turns.next.fetch_add(1, std::memory_order_relaxed)) { - while (_turns.serving.load(std::memory_order_acquire) != _turn) ::sched_yield(); + explicit Lock(Mutex& mutex) : _mutex(mutex) { + pthread_mutex_lock(&_mutex.mutex); + while (_mutex.faulting.load(std::memory_order_acquire) != 0) pthread_cond_wait(&_mutex.idle, &_mutex.mutex); } ~Lock() { - _turns.serving.store(_turn + 1, std::memory_order_release); + pthread_mutex_unlock(&_mutex.mutex); } - Turns& _turns; - std::uint64_t _turn; + Mutex& _mutex; + }; + + struct FaultLock { + explicit FaultLock(Mutex& mutex) : _mutex(mutex) { + _mutex.faulting.fetch_add(1, std::memory_order_acq_rel); + pthread_mutex_lock(&_mutex.mutex); + } + ~FaultLock() { + if (_mutex.faulting.fetch_sub(1, std::memory_order_acq_rel) == 1) pthread_cond_broadcast(&_mutex.idle); + pthread_mutex_unlock(&_mutex.mutex); + } + Mutex& _mutex; }; Watch() { @@ -530,7 +551,7 @@ private: bool record(std::uintptr_t address) { const auto page = pageStart(address); - const Lock lock(_lock); + const FaultLock lock(_lock); auto it = _ranges.upper_bound(page); if (it == _ranges.begin()) return false; --it; @@ -623,7 +644,7 @@ private: const std::uintptr_t _page = static_cast(::getpagesize()); bool _available = false; - Turns _lock; + Mutex _lock; std::map _ranges; std::vector> _hostWrites; struct sigaction _previousBus {}; diff --git a/core/libs/prx/libc/src/HeapDiagnostics.cpp b/core/libs/prx/libc/src/HeapDiagnostics.cpp index 135c46f928..04324b8d39 100644 --- a/core/libs/prx/libc/src/HeapDiagnostics.cpp +++ b/core/libs/prx/libc/src/HeapDiagnostics.cpp @@ -9,7 +9,11 @@ namespace { std::uint64_t mspaceAtomicIdMask = 0; -std::array mstateTable{}; +constexpr std::size_t MspaceTableCapacity = 65; +std::array mstateTable{}; + +static_assert(MspaceTableCapacity == 65); +static_assert(mstateTable.size() == MspaceTableCapacity); static_assert(sizeof(LibcHeapInfo) == 32); static_assert(offsetof(LibcHeapInfo, mspace_atomic_id_mask) == 16); diff --git a/core/libs/prx/libc/src/Interfaces.cpp b/core/libs/prx/libc/src/Interfaces.cpp index f61e17fbd4..8e39cfa29b 100644 --- a/core/libs/prx/libc/src/Interfaces.cpp +++ b/core/libs/prx/libc/src/Interfaces.cpp @@ -5,9 +5,17 @@ #include #else #include +#include #include +#include #include +#include +#include +#include +#include +#include #include +#include #endif #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -15,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +61,7 @@ struct Entry { std::vector address; std::vector netmask; std::vector destination; + std::vector data; }; std::vector GuestIpv4(const std::uint8_t* bytes) { @@ -150,6 +160,100 @@ int HostEntries(std::vector& entries) { return 0; } #else +constexpr std::uint8_t guestLinkFamily = 18; +constexpr std::size_t guestLinkNameArea = 8 + 16; +constexpr std::size_t guestLinkMinimum = 54; +constexpr std::uint8_t guestTypeOther = 0x1; +constexpr std::uint8_t guestTypeEthernet = 0x6; +constexpr std::uint8_t guestTypePpp = 0x17; +constexpr std::uint8_t guestTypeLoopback = 0x18; +constexpr std::uint8_t guestLinkStateDown = 1; +constexpr std::uint8_t guestLinkStateUp = 2; +constexpr std::size_t guestInterfaceDataSize = 152; +constexpr unsigned int hostLowerUp = 0x10000; + +struct HostInterface { + std::string name; + bool linked = false; + Entry link; + std::vector addresses; +}; + +template +void Store(std::vector& bytes, std::size_t offset, TValue value) { + std::memcpy(bytes.data() + offset, &value, sizeof(value)); +} + +std::vector GuestLinkAddress(const std::string& name, std::uint16_t index, std::uint8_t type, const std::uint8_t* hardware, std::size_t hardwareLength) { + std::size_t size = std::max(guestLinkMinimum, guestLinkNameArea + hardwareLength); + size = Aligned(size); + if (size > 0xFF) throw std::runtime_error("getifaddrs: link-level address of " + name + " does not fit sockaddr_dl"); + std::vector guest(size, 0); + guest[0] = static_cast(size); + guest[1] = guestLinkFamily; + Store(guest, 2, index); + guest[4] = type; + guest[5] = static_cast(name.size()); + guest[6] = static_cast(hardwareLength); + std::memcpy(guest.data() + 8, name.data(), name.size()); + if (hardwareLength != 0) std::memcpy(guest.data() + 8 + name.size(), hardware, hardwareLength); + return guest; +} + +std::optional ReadSysfsCounter(const std::string& name, const char* file) { + std::ifstream input("/sys/class/net/" + name + "/" + file); + long long value = 0; + if (!(input >> value)) return std::nullopt; + return static_cast(value); +} + +std::uint32_t HostMtu(const std::string& name) { + const int probe = ::socket(AF_INET, SOCK_DGRAM, 0); + if (probe < 0) throw std::runtime_error(std::string("getifaddrs: no socket to read the MTU of ") + name + ": " + std::strerror(errno)); + ifreq request{}; + std::memcpy(request.ifr_name, name.c_str(), name.size() + 1); + const int result = ::ioctl(probe, SIOCGIFMTU, &request); + const int error = errno; + ::close(probe); + if (result != 0) throw std::runtime_error(std::string("getifaddrs: SIOCGIFMTU failed on ") + name + ": " + std::strerror(error)); + return static_cast(request.ifr_mtu); +} + +std::vector GuestInterfaceData(const std::string& name, unsigned int hostFlags, const void* hostStats, std::uint8_t type, std::uint8_t hardwareLength) { + std::vector data(guestInterfaceDataSize, 0); + data[0] = type; + data[2] = hardwareLength; + data[3] = type == guestTypeEthernet ? 14 : 0; + if (type == guestTypeLoopback) data[4] = (hostFlags & IFF_UP) ? guestLinkStateUp : guestLinkStateDown; + else data[4] = (hostFlags & hostLowerUp) ? guestLinkStateUp : guestLinkStateDown; + Store(data, 6, static_cast(guestInterfaceDataSize)); + Store(data, 8, HostMtu(name)); + if (const auto speed = ReadSysfsCounter(name, "speed"); speed && static_cast(*speed) > 0) + Store(data, 16, *speed * 1000000u); + constexpr std::pair counters[] = { + {"statistics/rx_packets", 24}, {"statistics/rx_errors", 32}, {"statistics/tx_packets", 40}, {"statistics/tx_errors", 48}, + {"statistics/collisions", 56}, {"statistics/rx_bytes", 64}, {"statistics/tx_bytes", 72}, {"statistics/multicast", 80}, + {"statistics/rx_dropped", 96}, {"statistics/tx_dropped", 104}, + }; + bool complete = true; + for (const auto& [file, offset] : counters) { + const auto value = ReadSysfsCounter(name, file); + if (!value) { complete = false; break; } + Store(data, offset, *value); + } + if (!complete) { + if (!hostStats) throw std::runtime_error("getifaddrs: no counters for " + name + " in sysfs or the host list"); + rtnl_link_stats stats{}; + std::memcpy(&stats, hostStats, sizeof(stats)); + const std::pair fallback[] = { + {stats.rx_packets, 24}, {stats.rx_errors, 32}, {stats.tx_packets, 40}, {stats.tx_errors, 48}, {stats.collisions, 56}, + {stats.rx_bytes, 64}, {stats.tx_bytes, 72}, {stats.multicast, 80}, {stats.rx_dropped, 96}, {stats.tx_dropped, 104}, + }; + for (const auto& [value, offset] : fallback) Store(data, offset, static_cast(value)); + } + return data; +} + std::uint32_t InterfaceFlags(unsigned int host) { constexpr std::pair flags[] = { {IFF_UP, guestUp}, {IFF_BROADCAST, guestBroadcast}, {IFF_LOOPBACK, guestLoopback}, @@ -174,9 +278,31 @@ int HostEntries(std::vector& entries) { default: throw std::runtime_error(std::string("getifaddrs: host enumeration failed: ") + std::strerror(hostError)); } } + std::vector interfaces; + const auto interfaceFor = [&](const std::string& name) -> HostInterface& { + const auto found = std::find_if(interfaces.begin(), interfaces.end(), [&](const HostInterface& candidate) { return candidate.name == name; }); + if (found != interfaces.end()) return *found; + interfaces.push_back({name, false, {}, {}}); + return interfaces.back(); + }; try { for (auto* item = host; item; item = item->ifa_next) { - if (!item->ifa_addr || (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6)) continue; + if (!item->ifa_addr) continue; + if (item->ifa_addr->sa_family == AF_PACKET) { + const auto& link = *reinterpret_cast(item->ifa_addr); + HostInterface& owner = interfaceFor(CheckedName(item->ifa_name)); + if (owner.linked) throw std::runtime_error("getifaddrs: two link-level entries for " + owner.name); + if (link.sll_ifindex <= 0 || link.sll_ifindex > 0xFFFF) throw std::runtime_error("getifaddrs: interface index of " + owner.name + " does not fit sockaddr_dl"); + const std::uint8_t type = link.sll_hatype == ARPHRD_ETHER ? guestTypeEthernet : link.sll_hatype == ARPHRD_LOOPBACK ? guestTypeLoopback + : link.sll_hatype == ARPHRD_PPP ? guestTypePpp : guestTypeOther; + const std::uint8_t hardwareLength = type == guestTypeLoopback ? 0 : std::min(link.sll_halen, sizeof(link.sll_addr)); + owner.link = Entry{owner.name, InterfaceFlags(item->ifa_flags), + GuestLinkAddress(owner.name, static_cast(link.sll_ifindex), type, link.sll_addr, hardwareLength), {}, {}, + GuestInterfaceData(owner.name, item->ifa_flags, item->ifa_data, type, hardwareLength)}; + owner.linked = true; + continue; + } + if (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6) continue; if (!item->ifa_netmask) throw std::runtime_error(std::string("getifaddrs: host address without a netmask on ") + item->ifa_name); Entry entry{CheckedName(item->ifa_name), InterfaceFlags(item->ifa_flags), GuestAddress(item->ifa_addr), GuestAddress(item->ifa_netmask), {}}; const int family = item->ifa_addr->sa_family; @@ -187,13 +313,17 @@ int HostEntries(std::vector& entries) { entry.destination = item->ifa_broadaddr && item->ifa_broadaddr->sa_family == AF_INET ? GuestAddress(item->ifa_broadaddr) : BroadcastAddress(entry.address, entry.netmask); } - entries.push_back(std::move(entry)); + interfaceFor(entry.name).addresses.push_back(std::move(entry)); } } catch (...) { ::freeifaddrs(host); throw; } ::freeifaddrs(host); + for (auto& owner : interfaces) { + if (owner.linked) entries.push_back(std::move(owner.link)); + for (auto& address : owner.addresses) entries.push_back(std::move(address)); + } return 0; } #endif @@ -212,7 +342,7 @@ int APS5_VABI getifaddrs_nid_postfix(GuestInterfaceAddress** list) { } std::size_t size = entries.size() * sizeof(GuestInterfaceAddress); for (const auto& entry : entries) - size += Aligned(entry.name.size() + 1) + Aligned(entry.address.size()) + Aligned(entry.netmask.size()) + Aligned(entry.destination.size()); + size += Aligned(entry.name.size() + 1) + Aligned(entry.address.size()) + Aligned(entry.netmask.size()) + Aligned(entry.destination.size()) + Aligned(entry.data.size()); auto* block = static_cast(ApplicationHeapAllocate_nid_no_patch(size)); if (!block) { *__error_nid_postfix() = 12; @@ -233,9 +363,9 @@ int APS5_VABI getifaddrs_nid_postfix(GuestInterfaceAddress** list) { item.name = static_cast(place(entry.name.c_str(), entry.name.size() + 1)); item.flags = entry.flags; item.address = place(entry.address.data(), entry.address.size()); - item.netmask = place(entry.netmask.data(), entry.netmask.size()); + item.netmask = entry.netmask.empty() ? nullptr : place(entry.netmask.data(), entry.netmask.size()); item.destination = entry.destination.empty() ? nullptr : place(entry.destination.data(), entry.destination.size()); - item.data = nullptr; + item.data = entry.data.empty() ? nullptr : place(entry.data.data(), entry.data.size()); } *list = items; return 0; diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 5ae2fdf949..063eca4f01 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -349,6 +349,7 @@ long double APS5_VABI log1pl_nid_postfix(long double x) { return std::log1p(x); long double APS5_VABI log2l_nid_postfix(long double x) { return std::log2(x); } long double APS5_VABI lgammal_nid_postfix(long double x) { return std::lgamma(x); } long double APS5_VABI tgammal_nid_postfix(long double x) { return std::tgamma(x); } +float APS5_VABI tgammaf_nid_postfix(float x) { return static_cast(std::tgamma(static_cast(x))); } long double APS5_VABI hypotl_nid_postfix(long double x, long double y) { if (std::isinf(x) || std::isinf(y)) return std::numeric_limits::infinity(); return std::hypot(x, y); diff --git a/core/libs/prx/libc/src/Rename.cpp b/core/libs/prx/libc/src/Rename.cpp index f5d9545b5c..89ef0a55a2 100644 --- a/core/libs/prx/libc/src/Rename.cpp +++ b/core/libs/prx/libc/src/Rename.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/FilesystemError.hpp" +#include "prx/libc/include/RenameDot.hpp" #include "prx/libc/include/General.hpp" #include #include @@ -96,10 +97,16 @@ static int RenamePath_nid_no_patch(const std::filesystem::path& source, const st extern "C" int RenameGuestPath_nid_no_patch(const char* from, const char* to) { if (!from || !to) { errno = 14; return -1; } if (!*from || !*to) { errno = 2; return -1; } + if (const int dot = RenameDotError_nid_no_patch(from)) { errno = dot; return -1; } + if (const int dot = RenameDotError_nid_no_patch(to)) { errno = dot; return -1; } try { const auto source = ResolvePath_nid_no_patch(from); const auto destination = ResolvePath_nid_no_patch(to); - const int error = RenamePath_nid_no_patch(source, destination); + int error = RenamePath_nid_no_patch(source, destination); + if (error == 2) { + std::error_code status; + if (HasNonDirectoryPrefix_nid_no_patch(source) || (std::filesystem::exists(source, status) && HasNonDirectoryPrefix_nid_no_patch(destination))) error = 20; + } 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 6cccb21222..d040f9afe5 100644 --- a/core/libs/prx/libc/src/ScanningWide.cpp +++ b/core/libs/prx/libc/src/ScanningWide.cpp @@ -427,4 +427,14 @@ int APS5_VABI swscanf_s_nid_postfix(const char16_t* buffer, const char16_t* form } } +int APS5_VABI vswscanf_nid_postfix(const char16_t* buffer, const char16_t* format, VaList* args) { + if (buffer == nullptr || format == nullptr) return EOF; + return WideScanner("vswscanf", false, buffer, format, args).Run(); +} + +int APS5_VABI vswscanf_s_nid_postfix(const char16_t* buffer, const char16_t* format, VaList* args) { + if (buffer == nullptr || format == nullptr) return EOF; + return WideScanner("vswscanf_s", true, buffer, format, args).Run(); +} + } diff --git a/core/libs/prx/libc/src/StringExtensions.cpp b/core/libs/prx/libc/src/StringExtensions.cpp index f1f4eb975d..a6cb27bb7c 100644 --- a/core/libs/prx/libc/src/StringExtensions.cpp +++ b/core/libs/prx/libc/src/StringExtensions.cpp @@ -3,10 +3,31 @@ #include #include #include +#include +#include #include extern "C" { +long long APS5_VABI strtonum_nid_postfix(const char* text, long long minimum, long long maximum, const char** error) { + const int savedError = errno; + const auto fail = [error](const char* message, int number) -> long long { + if (error) *error = message; + errno = number; + return 0; + }; + if (minimum > maximum) return fail("invalid", 22); + errno = 0; + char* end = nullptr; + const long long value = std::strtoll(text, &end, 10); + if (errno == EINVAL || end == text || *end != '\0') return fail("invalid", 22); + if ((value == LLONG_MIN && errno == ERANGE) || value < minimum) return fail("too small", 34); + if ((value == LLONG_MAX && errno == ERANGE) || value > maximum) return fail("too large", 34); + if (error) *error = nullptr; + errno = savedError; + return value; +} + char* APS5_VABI basename_nid_postfix(const char* path) { thread_local char buffer[1024]; std::string_view name = path && *path ? path : "."; diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 40c192a30d..8774ef52ff 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -7,7 +7,9 @@ #include #include #include +#include #include +#include #include "prx/libc/include/General.hpp" #include "prx/libc/include/SonyCrt.hpp" @@ -37,31 +39,130 @@ std::string AsciiPrefix(const char16_t* text) { return prefix; } -bool IsNanSubject(const char* text) { +struct NanSubject { + const char* tail; + bool negative; +}; + +NanSubject FindNanSubject(const char* text) { while (*text == ' ' || (*text >= '\t' && *text <= '\r')) ++text; - if (*text == '+' || *text == '-') ++text; - return *text == 'n' || *text == 'N'; + bool negative = false; + if (*text == '+' || *text == '-') negative = *text++ == '-'; + if ((text[0] | 0x20) == 'n' && (text[1] | 0x20) == 'a' && (text[2] | 0x20) == 'n') return {text + 3, negative}; + return {nullptr, negative}; } -double QuietNan(double value, const char* text) { - const auto bits = std::bit_cast(value); - if ((bits & 0x7ff0000000000000u) != 0x7ff0000000000000u || !IsNanSubject(text)) return value; - return std::bit_cast((bits & 0x8000000000000000u) | 0x7ff8000000000000u); +unsigned HexNanDigit(unsigned char c) { + if (c >= '0' && c <= '9') return c - '0' + 0x10; + if (c >= 'a' && c <= 'f') return c - 'a' + 0x1a; + if (c >= 'A' && c <= 'F') return c - 'A' + 0x1a; + return 0; } -float QuietNan(float value, const char* text) { - const auto bits = std::bit_cast(value); - if ((bits & 0x7f800000u) != 0x7f800000u || !IsNanSubject(text)) return value; - return std::bit_cast((bits & 0x80000000u) | 0x7fc00000u); +void ShiftNanWords(std::uint32_t* words, int from, int to, int digits) { + const int shift = (8 - digits) * 4; + const int carry = 32 - shift; + int index = from; + do { + words[index] |= words[index + 1] << carry; + words[index + 1] >>= shift; + } while (++index < to); } -long double QuietNan(long double value, const char* text) { - std::uint16_t exponent; - std::memcpy(&exponent, reinterpret_cast(&value) + sizeof(std::uint64_t), sizeof(exponent)); - if ((exponent & 0x7fffu) != 0x7fffu || !IsNanSubject(text)) return value; - const std::uint64_t significand = 0xc000000000000000u; - std::memcpy(&value, &significand, sizeof(significand)); - return value; +std::uint64_t HexNanPayload(const char* open, const char*& end) { + std::uint32_t words[2] = {}; + constexpr int high = 1; + int top = high; + int group = high; + int digits = 0; + int groupStart = 0; + int inWord = 0; + const auto* s = reinterpret_cast(open); + const auto skipBlanksAndPrefix = [&s](bool inside) { + while (s[1] != 0 && s[1] <= ' ') ++s; + if (inside && s[1] == 0) throw std::invalid_argument("strtod: blanks after a NAN payload digit reach the terminating NUL, where the console reads past it"); + if (s[1] == '0' && (s[2] == 'x' || s[2] == 'X') && s[3] > ' ') s += 2; + }; + skipBlanksAndPrefix(false); + for (unsigned char c; (c = *++s) != 0;) { + const unsigned digit = HexNanDigit(c); + if (digit == 0) { + if (c <= ' ') { + if (groupStart < digits) { + if (top < group && inWord < 8) ShiftNanWords(words, top, group, inWord); + if (top <= 0) { + inWord = 8; + continue; + } + groupStart = digits; + words[--top] = 0; + group = top; + inWord = 0; + } + skipBlanksAndPrefix(true); + continue; + } + if (c == ')' && digits != 0) { + end = reinterpret_cast(s + 1); + break; + } + for (; c != 0; c = *++s) { + if (c == ')') { + end = reinterpret_cast(s + 1); + break; + } + } + return 0; + } + ++digits; + if (++inWord > 8) { + if (top <= 0) continue; + inWord = 1; + words[--top] = 0; + } + words[top] = (words[top] << 4) | (digit & 0xf); + } + if (digits == 0) return 0; + if (top < group && inWord < 8) ShiftNanWords(words, top, group, inWord); + if (top > 0) { + int to = 0; + int from = top; + do words[to++] = words[from++]; + while (from <= high); + do words[to++] = 0; + while (to <= high); + } else { + words[high] &= 0xffffffffu >> 12; + } + if (words[0] == 0 && words[1] == 0) words[0] = 1; + return (static_cast(words[1]) << 32) | words[0]; +} + +const char* NanSequenceEnd(const char* tail) { + if (*tail != '(') return tail; + const char* cursor = tail + 1; + while ((*cursor >= '0' && *cursor <= '9') || ((*cursor | 0x20) >= 'a' && (*cursor | 0x20) <= 'z') || *cursor == '_') ++cursor; + return *cursor == ')' ? cursor + 1 : tail; +} + +template +TFloat NanValue(const NanSubject& subject, const char*& end) { + if constexpr (std::is_same_v) { + end = subject.tail; + const std::uint64_t payload = *subject.tail == '(' ? HexNanPayload(subject.tail, end) : 0; + return std::bit_cast((subject.negative ? 0x8000000000000000ull : 0) | 0x7ff8000000000000ull | payload); + } else if constexpr (std::is_same_v) { + end = NanSequenceEnd(subject.tail); + return std::bit_cast(0x7fc00000u); + } else { + end = NanSequenceEnd(subject.tail); + TFloat value{}; + const std::uint64_t significand = 0xc000000000000000u; + const std::uint16_t signExponent = 0x7fff; + std::memcpy(&value, &significand, sizeof(significand)); + std::memcpy(reinterpret_cast(&value) + sizeof(significand), &signExponent, sizeof(signExponent)); + return value; + } } template @@ -75,10 +176,16 @@ auto ParseAsciiPrefix(const char16_t* text, char16_t** end, TParse parse) { template TFloat ParseFloat(const char* text, char** end, TFloat (*parse)(const char*, char**)) { + if (const NanSubject subject = FindNanSubject(text); subject.tail != nullptr) { + const char* stop = subject.tail; + const TFloat value = NanValue(subject, stop); + if (end != nullptr) *end = const_cast(stop); + return value; + } char* parsedEnd = nullptr; const TFloat value = parse(text, &parsedEnd); if (end != nullptr) *end = parsedEnd; - return parsedEnd == text ? TFloat{0} : QuietNan(value, text); + return parsedEnd == text ? TFloat{0} : value; } } @@ -153,7 +260,7 @@ const char* APS5_VABI strchr_nid_postfix(const char* s, int c) { } char* APS5_VABI strrchr_nid_postfix(const char* s, int c) { - return std::strrchr(const_cast(s), c); + return std::strrchr(const_cast(s), static_cast(c)); } char* APS5_VABI rindex_nid_postfix(const char* s, int c) { diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 167360e72c..c12cf7c03c 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -1,5 +1,8 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/General.hpp" +#ifdef _WIN32 +#include "prx/libc/include/PreciseWait.hpp" +#endif #include #include #include @@ -14,6 +17,12 @@ #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif namespace { @@ -54,19 +63,68 @@ constexpr int mutexRecursive = 0x100; constexpr std::uint64_t mutexTag = 0x5854'4D5F'4D54'5844ull; constexpr std::uint64_t conditionTag = 0x5854'4E43'5F44'4E43ull; +#ifdef _WIN32 +struct DinkumwareNativeMutex { + CRITICAL_SECTION value; + + DinkumwareNativeMutex() { + if (!InitializeCriticalSectionAndSpinCount(&value, 0)) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "_Mtx_init: initializing critical section failed"); + } + + ~DinkumwareNativeMutex() { DeleteCriticalSection(&value); } + void lock() { EnterCriticalSection(&value); } + void unlock() { LeaveCriticalSection(&value); } + bool try_lock() { return TryEnterCriticalSection(&value) != FALSE; } +}; + +struct DinkumwareNativeCondition { + CONDITION_VARIABLE value = CONDITION_VARIABLE_INIT; +}; +#else +using DinkumwareNativeMutex = std::mutex; +using DinkumwareNativeCondition = std::condition_variable; +#endif + struct DinkumwareMutex { std::uint64_t tag = mutexTag; int type = 0; int count = 0; std::atomic owner{}; - std::mutex native; + DinkumwareNativeMutex native; }; struct DinkumwareCondition { std::uint64_t tag = conditionTag; - std::condition_variable native; + DinkumwareNativeCondition native; }; +#ifdef _WIN32 +DWORD WaitMillisecondsUntil(std::chrono::system_clock::time_point deadline) { + const auto now = std::chrono::system_clock::now(); + if (now >= deadline) + return 0; + const auto remaining = std::chrono::ceil(deadline - now).count(); + constexpr auto largest = static_cast(std::numeric_limits::max() - 1U); + return static_cast(std::min(remaining, largest)); +} + +bool WaitUntil(DinkumwareCondition* condition, DinkumwareMutex* mutex, std::chrono::system_clock::time_point deadline) { + for (;;) { + const DWORD milliseconds = WaitMillisecondsUntil(deadline); + if (milliseconds == 0) + return false; + if (SleepConditionVariableCS(&condition->native.value, &mutex->native.value, milliseconds) != FALSE) + return true; + const DWORD error = GetLastError(); + if (error != ERROR_TIMEOUT) + throw std::system_error(static_cast(error), std::system_category(), "_Cnd_timedwait: condition wait failed"); + if (std::chrono::system_clock::now() >= deadline) + return false; + } +} +#endif + DinkumwareMutex* ResolveMutex(void** handle, const char* function) { if (!handle) throw std::invalid_argument(std::string(function) + ": null mutex handle pointer"); @@ -213,7 +271,12 @@ int APS5_VABI _Cnd_wait_nid_postfix(void** conditionHandle, void** mutexHandle) mutex->owner.store(std::thread::id{}, std::memory_order_release); std::unique_lock lock(mutex->native, std::adopt_lock); try { +#ifdef _WIN32 + if (SleepConditionVariableCS(&condition->native.value, &mutex->native.value, INFINITE) == FALSE) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "_Cnd_wait: condition wait failed"); +#else condition->native.wait(lock); +#endif } catch (const std::system_error&) { lock.release(); mutex->owner.store(std::this_thread::get_id(), std::memory_order_release); @@ -252,7 +315,11 @@ int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHan const auto deadlineTime = std::chrono::system_clock::time_point( std::chrono::seconds(deadline.tv_sec) + std::chrono::ceil(std::chrono::nanoseconds(deadline.tv_nsec))); +#ifdef _WIN32 + timeout = !WaitUntil(condition, mutex, deadlineTime); +#else timeout = (condition->native.wait_until(lock, deadlineTime) == std::cv_status::timeout); +#endif } catch (const std::system_error&) { lock.release(); mutex->owner.store(std::this_thread::get_id(), std::memory_order_release); @@ -266,12 +333,20 @@ int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHan } int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle) { +#ifdef _WIN32 + WakeAllConditionVariable(&ResolveCondition(handle, "_Cnd_broadcast")->native.value); +#else ResolveCondition(handle, "_Cnd_broadcast")->native.notify_all(); +#endif return threadSuccess; } int SignalCondition_nid_no_patch(void** handle) { +#ifdef _WIN32 + WakeConditionVariable(&ResolveCondition(handle, "_Cnd_signal")->native.value); +#else ResolveCondition(handle, "_Cnd_signal")->native.notify_one(); +#endif return threadSuccess; } @@ -291,11 +366,16 @@ int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime*) { --remaining.tv_sec; } remaining.tv_sec = std::min(remaining.tv_sec, longestSleepSeconds); +#ifdef _WIN32 + PreciseSleepHundredNanos(static_cast(remaining.tv_sec) * 10000000ULL + + (static_cast(remaining.tv_nsec) + 99ULL) / 100ULL); +#else if (nanosleep(&remaining, nullptr) != 0) { if (errno == EINTR) continue; throw std::runtime_error("_Thrd_sleep: nanosleep failed"); } +#endif } } diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index fca1328c65..f49335af48 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -587,7 +587,8 @@ GuestTm* APS5_VABI localtime_s_nid_postfix(const int64_t* timer, GuestTm* result const int callerError = errno; #ifdef _WIN32 const std::int64_t cycles = *timer >= 0 && *timer <= HOST_LAST_SECOND ? 0 : calendarCycles(*timer, CALENDAR_CYCLE_SECONDS); - const std::time_t t = static_cast(*timer - cycles * CALENDAR_CYCLE_SECONDS); + const std::int64_t offset = *timer % CALENDAR_CYCLE_SECONDS; + const std::time_t t = static_cast(cycles == 0 ? *timer : offset < 0 ? offset + CALENDAR_CYCLE_SECONDS : offset); if (localtime_s(&host, &t) != 0) { errno = callerError; return nullptr; diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 3d685d640a..7dd8affa4c 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -382,8 +382,10 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { const char* kind = record->ExceptionInformation[0] == 0 ? "read" : record->ExceptionInformation[0] == 1 ? "write" : "execute"; Report(" %s of 0x%016llx\n", kind, static_cast(record->ExceptionInformation[1])); std::uint8_t trap[2]; - if (record->ExceptionInformation[0] == 0 && record->ExceptionInformation[1] == ~0ull && ReadCode(context->Rip, trap, sizeof(trap)) == sizeof(trap) && trap[0] == 0xcd && trap[1] == 0x45) - Report(" guest abort() (int 0x45)\n"); + if (record->ExceptionInformation[0] == 0 && record->ExceptionInformation[1] == ~0ull && ReadCode(context->Rip, trap, sizeof(trap)) == sizeof(trap) && trap[0] == 0xcd) { + if (trap[1] == 0x45) Report(" guest abort() (int 0x45)\n"); + else if (trap[1] == 0x41) Report(" guest assert/break (int 0x41)\n"); + } } Report(" rax %016llx rbx %016llx rcx %016llx rdx %016llx\n", context->Rax, context->Rbx, context->Rcx, context->Rdx); Report(" rsi %016llx rdi %016llx rbp %016llx rsp %016llx\n", context->Rsi, context->Rdi, context->Rbp, context->Rsp); diff --git a/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp b/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp index 0cceb906ff..ef97a2c7cf 100644 --- a/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp +++ b/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp @@ -11,6 +11,10 @@ struct AppTitleId { char value[12]; }; +struct AppContentVersion { + char value[16]; +}; + struct AppIconData { const std::uint8_t* bytes; std::uint64_t size; @@ -20,6 +24,8 @@ extern "C" { AppTitle GetAppTitle_nid_postfix(); AppTitleId GetAppTitleId_nid_postfix(); +bool HasAppParamJson_nid_no_patch(); +AppContentVersion GetAppContentVersion_nid_no_patch(); bool HasAppIcon_nid_postfix(); AppIconData GetAppIconData_nid_postfix(); std::uint64_t GetAppDownloadDataSizeMiB_nid_postfix(); diff --git a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp index 22f615c522..65e2c6c0a8 100644 --- a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp +++ b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp @@ -9,6 +9,7 @@ struct ParsedParamJson { std::string title; std::string titleId; + std::string contentVersion; std::uint64_t downloadDataSizeMiB = 0; std::uint32_t attribute3 = 0; std::optional flexibleMemorySizeBytes; diff --git a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp index f078143320..3f46122f14 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp @@ -20,6 +20,7 @@ bool g_iconAttempted = false; bool g_iconLoaded = false; char g_title[128] = {}; char g_titleId[12] = {}; +char g_contentVersion[16] = {}; std::uint64_t g_downloadDataSizeMiB = 0; std::uint32_t g_attribute3 = 0; std::int32_t g_userDefinedParams[4] = {}; @@ -47,6 +48,7 @@ void ensureTitleLoaded() { const auto parsed = parseParams(ResolvePath_nid_no_patch(AppMetadataParamJsonGuestPath), ResolvePath_nid_no_patch(AppMetadataParamSfoGuestPath)); copyToFixedBuffer(g_title, sizeof(g_title), parsed.title); copyToFixedBuffer(g_titleId, sizeof(g_titleId), parsed.titleId); + if (!parsed.contentVersion.empty() && parsed.contentVersion.size() < sizeof(g_contentVersion)) copyToFixedBuffer(g_contentVersion, sizeof(g_contentVersion), parsed.contentVersion); g_downloadDataSizeMiB = parsed.downloadDataSizeMiB; g_attribute3 = parsed.attribute3; std::memcpy(g_userDefinedParams, parsed.userDefinedParams, sizeof(g_userDefinedParams)); @@ -133,6 +135,17 @@ AppTitleId GetAppTitleId_nid_postfix() { return result; } +bool HasAppParamJson_nid_no_patch() { + return std::filesystem::exists(ResolvePath_nid_no_patch(AppMetadataParamJsonGuestPath)); +} + +AppContentVersion GetAppContentVersion_nid_no_patch() { + ensureTitleLoaded(); + AppContentVersion result{}; + std::memcpy(result.value, g_contentVersion, sizeof(result.value)); + return result; +} + bool HasAppIcon_nid_postfix() { ensureIconLoaded(); return g_iconLoaded; diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index e8ac7d212a..3150e1419a 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -322,6 +322,7 @@ ParsedParamJson parseParamJson(const std::filesystem::path& paramJsonPath) { ParsedParamJson result; result.title = title; result.titleId = titleId; + if (const JsonValue* contentVersion = findObjectMember(root, "contentVersion"); contentVersion != nullptr && contentVersion->type == JsonType::String) result.contentVersion = contentVersion->stringValue; if (const JsonValue* attribute = findObjectMember(root, "attribute3")) { const auto& number = attribute->numberText; if (attribute->type != JsonType::Number || number.find_first_of("-.eE") != std::string::npos) throw std::runtime_error("param.json attribute3 is not an unsigned integer"); diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 714c8d43fa..3b2ed8af91 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1323,6 +1323,16 @@ if(BUILD_TESTING) set_tests_properties(guest_udp PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_socket_address_length_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketAddressLength.cpp) + target_include_directories(guest_socket_address_length_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_address_length_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_address_length_tests) + add_test(NAME guest_socket_address_length COMMAND guest_socket_address_length_tests) + set_tests_properties(guest_socket_address_length PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_socket_address_length PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_socket_read_write_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketReadWrite.cpp) target_include_directories(guest_socket_read_write_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_socket_read_write_tests PRIVATE libkernel libc) @@ -1407,6 +1417,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test_executable(guest_socket_on_non_socket_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketOnNonSocket.cpp) + target_include_directories(guest_socket_on_non_socket_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_on_non_socket_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_on_non_socket_tests) + add_test(NAME guest_socket_on_non_socket COMMAND guest_socket_on_non_socket_tests) + set_tests_properties(guest_socket_on_non_socket PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_socket_on_non_socket PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_dup_tests ${LIBS_INCLUDE_DIR}/tests/GuestDup.cpp) target_include_directories(guest_dup_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_dup_tests PRIVATE libkernel libc) @@ -1642,6 +1662,16 @@ if(BUILD_TESTING) target_compile_options(guest_positioned_close_tests PRIVATE -fasynchronous-unwind-tables) endif() endif() + add_test_executable(guest_path_name_length_tests ${LIBS_INCLUDE_DIR}/tests/GuestPathNameLength.cpp) + target_include_directories(guest_path_name_length_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_path_name_length_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_path_name_length_tests) + add_test(NAME guest_path_name_length COMMAND guest_path_name_length_tests) + set_tests_properties(guest_path_name_length PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_path_name_length PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_file_vector_tests ${LIBS_INCLUDE_DIR}/tests/GuestFileVector.cpp) target_include_directories(guest_file_vector_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_file_vector_tests PRIVATE libkernel libc) @@ -1661,6 +1691,26 @@ if(BUILD_TESTING) set_tests_properties(guest_fstat_fault PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_rename_dot_tests ${LIBS_INCLUDE_DIR}/tests/GuestRenameDot.cpp) + target_include_directories(guest_rename_dot_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_rename_dot_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_rename_dot_tests) + add_test(NAME guest_rename_dot COMMAND guest_rename_dot_tests) + set_tests_properties(guest_rename_dot PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_rename_dot PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_truncate_extend_tests ${LIBS_INCLUDE_DIR}/tests/GuestTruncateExtend.cpp) + target_include_directories(guest_truncate_extend_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_truncate_extend_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_truncate_extend_tests) + add_test(NAME guest_truncate_extend COMMAND guest_truncate_extend_tests) + set_tests_properties(guest_truncate_extend PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_truncate_extend PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_fstatfs_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatfs.cpp) target_include_directories(guest_fstatfs_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_fstatfs_tests PRIVATE libkernel libc) @@ -1722,6 +1772,13 @@ if(BUILD_TESTING) add_test(NAME guest_mutex_raise_exception COMMAND guest_mutex_raise_exception_tests) set_tests_properties(guest_mutex_raise_exception PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + 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) + configure_windows_unwind(guest_raise_in_host_code_tests) + add_test(NAME guest_raise_in_host_code COMMAND guest_raise_in_host_code_tests) + set_tests_properties(guest_raise_in_host_code PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") add_test_executable(guest_mutex_contention_tests ${LIBS_INCLUDE_DIR}/tests/GuestMutexContention.cpp) target_include_directories(guest_mutex_contention_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_mutex_contention_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index 125f94982a..604e85b34c 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -35,7 +35,7 @@ std::vector DirectMemoryViews(int64_t start, size_t len); void UnmapDirectMemoryViews(const std::vector& views); bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType); void DirectMemoryRetype(int64_t start, size_t len, int memoryType); -bool DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut); +int DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut); size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment); int DoMapAnon(void** addr, size_t len, int prot, int flags, size_t alignment = PS5_PAGE_SIZE, bool exclusive = false, bool allowStackReplacement = false); diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index e0bd3358f1..79d07266cb 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -221,7 +221,8 @@ int APS5_VABI sceKernelAvailableDirectMemorySize(int64_t search_start, int64_t s if (!phys_addr_out || !size_out) return SCE_KERNEL_ERROR_EINVAL; int64_t start = 0; size_t size = 0; - if (!DirectMemoryLargestFree(search_start, search_end, alignment, &start, &size)) { *phys_addr_out = 0; *size_out = 0; return SCE_KERNEL_ERROR_EAGAIN; } + const int result = DirectMemoryLargestFree(search_start, search_end, alignment, &start, &size); + if (result != 0) { *phys_addr_out = 0; *size_out = 0; return result; } *phys_addr_out = start; *size_out = size; return 0; diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index 70294ec612..54cdbaf12d 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -153,14 +153,14 @@ struct PhysicalMemoryPool { return true; } - bool LargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { - if (!startOut || !sizeOut) return false; - if (searchStart < 0 || searchEnd <= searchStart) return false; + int LargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { + if (!startOut || !sizeOut) return SCE_KERNEL_ERROR_EINVAL; + if (searchStart < 0 || searchEnd <= searchStart) return SCE_KERNEL_ERROR_EAGAIN; uint64_t align; - if (!NormalizeAlignment(alignment, align)) return false; + if (!NormalizeAlignment(alignment, align)) return SCE_KERNEL_ERROR_EAGAIN; uint64_t cur; - if (!RoundUp(static_cast(searchStart), kPage, cur)) return false; + if (!RoundUp(static_cast(searchStart), kPage, cur)) return SCE_KERNEL_ERROR_EAGAIN; const uint64_t limit = std::min(static_cast(searchEnd), kPoolBytes) & ~(kPage - 1); std::lock_guard lock(_mutex); @@ -184,10 +184,10 @@ struct PhysicalMemoryPool { cur = runEnd; } - if (bestSize == 0) return false; + if (bestSize == 0) return SCE_KERNEL_ERROR_ENOMEM; *startOut = static_cast(bestStart); *sizeOut = static_cast(bestSize); - return true; + return 0; } size_t FreeRun(uint64_t offset, uint64_t limit) { @@ -343,7 +343,7 @@ void DirectMemoryRetype(int64_t start, size_t len, int memoryType) { (void)PhysicalMemoryPool::Instance().Retype(static_cast(start), len, memoryType); } -bool DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { +int DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { return PhysicalMemoryPool::Instance().LargestFree(searchStart, searchEnd, alignment, startOut, sizeOut); } diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index 2b5f864d8e..ec37c3ac94 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -3,6 +3,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/HostStreams.hpp" #include #include @@ -630,13 +631,16 @@ int APS5_VABI sceKernelDeleteAmprSystemEvent(KernelEqueue eq, int id) { static int AddDescriptorEvent(KernelEqueue eq, int fd, std::size_t size, void* udata, int16_t filter) { const bool isSocket = GuestSockets::IsOpen(fd); - if (!isSocket && DescriptorIsOpen_nid_no_patch(fd)) { - NotImplemented_nid_no_patch(filter == EVFILT_READ ? "sceKernelAddReadEvent: non-socket descriptor" : "sceKernelAddWriteEvent: non-socket descriptor"); + std::int64_t available = 0; + bool eof = false; + const bool isPipe = !isSocket && filter == EVFILT_READ && !File::IsHostStream(fd) && GuestSockets::PipeReadState(fd, &available, &eof); + if (!isSocket && !isPipe && DescriptorIsOpen_nid_no_patch(fd)) { + NotImplemented_nid_no_patch(filter == EVFILT_READ ? "sceKernelAddReadEvent: descriptor that is neither a socket nor the read end of a pipe" : "sceKernelAddWriteEvent: non-socket descriptor"); } - if (size > 1) { + if (size > 1 && !isPipe) { NotImplemented_nid_no_patch(filter == EVFILT_READ ? "sceKernelAddReadEvent: low watermark" : "sceKernelAddWriteEvent: low watermark"); } - if (!isSocket) { + if (!isSocket && !isPipe) { return SCE_KERNEL_ERROR_EBADF; } return EqueueAddEvent_nid_postfix(eq, EqueueDescriptorEvent(fd, filter, EV_ADD, udata)); diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index 81a9018c42..341324638c 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -9,6 +9,7 @@ enum class FcntlArgument { None, Int, Pointer }; FcntlArgument FcntlArgumentOf(int command); int Fcntl(int fd, int command, std::intptr_t argument); +bool IoctlRequestMalformed(std::uint64_t request); int Ioctl(int fd, std::uint64_t request, void* argument); int DuplicateFrom(int fd, int start); void RememberDescriptor(int fd, int openFlags); diff --git a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp index f6ab25f51a..159adb1852 100644 --- a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp +++ b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp @@ -6,6 +6,7 @@ #include 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 OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std::uint16_t mode); namespace File { diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index e4824dcb5b..a83f899801 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -93,6 +93,8 @@ constexpr std::uint32_t KeepOnExecRequest = 0x20006602; constexpr std::uint32_t NonblockingRequest = 0x8004667e; constexpr std::uint32_t ReadableRequest = 0x4004667f; constexpr std::uint32_t RequestIn = 0x80000000u; +constexpr std::uint32_t RequestOut = 0x40000000u; +constexpr std::uint32_t RequestVoid = 0x20000000u; constexpr std::uint32_t TerminalGroup = 't'; struct GuestLock { @@ -510,7 +512,16 @@ int Fcntl(int fd, int command, std::intptr_t argument) { } } +bool IoctlRequestMalformed(std::uint64_t request) { + const auto command = static_cast(request); + const std::uint32_t length = (command >> 16) & 0x1fff; + if ((command & (RequestIn | RequestOut | RequestVoid)) == 0) return true; + if ((command & RequestOut) && length == 0) return true; + return (command & RequestVoid) && length != 0 && length != sizeof(int); +} + int Ioctl(int fd, std::uint64_t request, void* argument) { + if (IoctlRequestMalformed(request)) return Fail(GuestEnotty); const auto command = static_cast(request); const std::uint32_t length = (command >> 16) & 0x1fff; if ((command & RequestIn) && length != 0 && argument == nullptr) return Fail(GuestEfault); diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index ae7e0f4117..cca49c2431 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -227,3 +227,13 @@ extern "C" const char* GetDirectoryDescriptorPath_nid_no_patch(int fd, int* erro *error = 0; return path.c_str(); } + +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 +} diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index b7c15b3403..04192be62e 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -28,6 +28,7 @@ #include #include #include +#include "prx/libkernel/File/include/WindowsSymbolicLink.hpp" static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint16_t mode) { const int access = nativeFlags & (_O_RDONLY | _O_WRONLY | _O_RDWR); const DWORD desired = access == _O_RDONLY ? GENERIC_READ : access == _O_WRONLY ? GENERIC_WRITE : GENERIC_READ | GENERIC_WRITE; @@ -112,6 +113,13 @@ static std::filesystem::path NativeMetadataPath(const std::filesystem::path& pat return native.starts_with(L"\\\\") ? std::filesystem::path(L"\\\\?\\UNC\\" + native.substr(2)) : std::filesystem::path(L"\\\\?\\" + native); } +static bool NativeSymbolicLinkLoop(const std::filesystem::path& path) { + const auto handle = 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 (handle == INVALID_HANDLE_VALUE) return GetLastError() == ERROR_CANT_RESOLVE_FILENAME; + CloseHandle(handle); + return false; +} static int MapFlags(int sceFlags) { int f = 0; const int acc = sceFlags & SCE_KERNEL_O_ACCMODE; @@ -213,6 +221,13 @@ static int OpenNative(const std::filesystem::path& native, int flags, std::uint1 constexpr int GuestEmlink = 31; std::error_code error; if (std::filesystem::is_symlink(native, error)) return SceErrorFromErrno(GuestEmlink); +#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, "open")) return SceErrorFromErrno(GuestEmlink); + } +#endif } int fd = NativeOpen(native, MapFlags(flags), mode); #ifdef _WIN32 @@ -228,7 +243,7 @@ static int OpenNative(const std::filesystem::path& native, int flags, std::uint1 } #endif if (fd < 0) { - return SceErrorFromErrno(errno); + return SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno); } if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY || (flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_TRUNC))) RecordWrittenPath_nid_no_patch(native); @@ -370,7 +385,10 @@ int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { if (error == std::errc::not_a_directory) return SceErrorFromErrno(GuestEnotdir); if (error == std::errc::filename_too_long || error == std::errc::too_many_symbolic_link_levels) return SceKernelError(FilesystemError(error)); if (!std::filesystem::exists(status)) { - return SceErrorFromErrno(2); +#ifdef _WIN32 + if (NativeSymbolicLinkLoop(metadataPath)) return SCE_KERNEL_ERROR_ELOOP; +#endif + return SceErrorFromErrno(NotFoundError_nid_no_patch(native)); } const std::string_view guestPath(path); if (!guestPath.empty() && guestPath.back() == '/' && !std::filesystem::is_directory(status)) return SceErrorFromErrno(GuestEnotdir); @@ -388,7 +406,7 @@ int APS5_VABI sceKernelUnlink(const char* path) { std::error_code error; if (std::filesystem::is_directory(std::filesystem::symlink_status(native, error))) return SceErrorFromErrno(GuestEperm); if (NativeUnlink(native) != 0) { - return SceErrorFromErrno(errno); + return SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno); } RecordWrittenPath_nid_no_patch(native); return 0; diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index dee7b33df1..4d786eb038 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -5,6 +5,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" +#include "prx/libc/include/RenameDot.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/File/include/File.hpp" @@ -136,7 +137,17 @@ static int NativeFtruncate(int descriptor, std::int64_t length) { if (handle == INVALID_HANDLE_VALUE) return EBADF; if (::GetFileType(handle) != FILE_TYPE_DISK) return EINVAL; if (WindowsHandleOnlyAppends_nid_no_patch(WindowsHandleAccess_nid_no_patch(handle, descriptor, "ftruncate"))) return TruncateThroughWriter(handle, length); - return static_cast(WithoutParameterHandler([descriptor, length] { return ::_chsize_s(descriptor, length); })); + FILE_END_OF_FILE_INFO end{}; + end.EndOfFile.QuadPart = length; + if (::SetFileInformationByHandle(handle, FileEndOfFileInfo, &end, sizeof(end))) return 0; + switch (::GetLastError()) { + case ERROR_ACCESS_DENIED: return EACCES; + case ERROR_DISK_FULL: + case ERROR_HANDLE_DISK_FULL: return ENOSPC; + case ERROR_FILE_TOO_LARGE: + case ERROR_INVALID_PARAMETER: return EFBIG; + default: return EINVAL; + } } static int NativeTruncate(const std::filesystem::path& path, std::int64_t length) { const int descriptor = ::_wopen(path.c_str(), _O_WRONLY | _O_BINARY); @@ -186,6 +197,8 @@ static int NativeSync(int descriptor) { handle = CreateFileW(directory->c_str(), GENERIC_WRITE, share, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); } else { const int previous = errno; + const auto pipe = reinterpret_cast(WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor); })); + if (pipe != INVALID_HANDLE_VALUE && GetFileType(pipe) == FILE_TYPE_PIPE) { errno = EINVAL; return -1; } if (::_commit(descriptor) == 0) return 0; const auto original = reinterpret_cast(::_get_osfhandle(descriptor)); if (original == INVALID_HANDLE_VALUE || GetFileType(original) != FILE_TYPE_DISK) { errno = EBADF; return -1; } @@ -484,18 +497,27 @@ static int NativeFstatfs(int descriptor, KernelStatfs* fs) { fs->f_namemax = nameMax; return 0; } + +static bool NativeIsPipe(int descriptor) { + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + return handle != INVALID_HANDLE_VALUE && ::GetFileType(handle) == FILE_TYPE_PIPE; +} #else #include #include -static int NativeFstatfs(int descriptor, KernelStatfs* fs) { +static bool NativeIsPipe(int descriptor) { constexpr long PipeFilesystem = 0x50495045; + struct stat file{}; + struct statfs kind{}; + return ::fstat(descriptor, &file) == 0 && S_ISFIFO(file.st_mode) && ::fstatfs(descriptor, &kind) == 0 + && static_cast(kind.f_type) == PipeFilesystem; +} + +static int NativeFstatfs(int descriptor, KernelStatfs* fs) { struct stat file{}; if (::fstat(descriptor, &file) != 0) return SceErrorFromErrno(errno) & 0xffff; if (S_ISSOCK(file.st_mode) && (::fcntl(descriptor, F_GETFL) & O_PATH) == 0) return GUEST_EINVAL; - if (S_ISFIFO(file.st_mode)) { - struct statfs kind{}; - if (::fstatfs(descriptor, &kind) == 0 && static_cast(kind.f_type) == PipeFilesystem) return GUEST_EINVAL; - } + if (NativeIsPipe(descriptor)) return GUEST_EINVAL; struct statvfs host{}; if (::fstatvfs(descriptor, &host) != 0) return SceErrorFromErrno(errno) & 0xffff; if (host.f_flag & ST_RDONLY) fs->f_flags |= KERNEL_MNT_RDONLY; @@ -605,25 +627,44 @@ static std::string FileFlagNames(std::uint64_t flags) { return names; } +static int SetFileFlags(const char* function, const std::string& target, std::uint64_t flags) { + std::uint64_t known = 0; + for (const auto& entry : GUEST_FILE_FLAGS) known |= entry.first; + if ((flags & ~known) != 0) return GUEST_EOPNOTSUPP; + if ((flags & GUEST_SF_SNAPSHOT) != 0) return GUEST_EPERM; + if (flags != 0) + throw std::runtime_error(std::string(function) + ": setting " + FileFlagNames(flags) + " on " + target + " is not supported, the host keeps no BSD file flags"); + return 0; +} + extern "C" { int APS5_VABI chmod_nid_postfix(const char* path, int mode) { if (const int error = PathError(path)) return PosixFailure(error); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); - if (NativeChmod(native, mode & 07777) != 0) return PosixResult(SceErrorFromErrno(errno)); + if (NativeChmod(native, mode & 07777) != 0) return PosixResult(SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno)); return 0; } int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { if (const int error = PathError(path)) return PosixFailure(error); - if (const int error = LookupError(ResolvePath_nid_no_patch(path))) return PosixFailure(error); - std::uint64_t known = 0; - for (const auto& entry : GUEST_FILE_FLAGS) known |= entry.first; - if ((flags & ~known) != 0) return PosixFailure(GUEST_EOPNOTSUPP); - if ((flags & GUEST_SF_SNAPSHOT) != 0) return PosixFailure(GUEST_EPERM); - if (flags != 0) - throw std::runtime_error("chflags: setting " + FileFlagNames(flags) + " on " + path + " is not supported, the host keeps no BSD file flags"); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); + if (const int error = LookupError(native)) return PosixFailure(error); + if (const int error = SetFileFlags("chflags", path, flags)) return PosixFailure(error); + return 0; +} + +int APS5_VABI fchflags_nid_postfix(int d, std::uint64_t flags) { + if (GuestSockets::IsOpen(d) || File::IsSharedMemoryDescriptor_nid_no_patch(d)) return PosixFailure(GUEST_EINVAL); + File::RefuseArchivedFile(d, "fchflags"); + if (!File::IsVnode(d)) { + if (!DescriptorIsOpen_nid_no_patch(d)) return PosixFailure(GUEST_EBADF); + if (NativeIsPipe(d)) return PosixFailure(GUEST_EINVAL); + throw std::runtime_error("fchflags: descriptor " + std::to_string(d) + " is neither a file, a directory, a pipe, a socket nor shared memory"); + } + if (const int error = SetFileFlags("fchflags", "descriptor " + std::to_string(d), flags)) return PosixFailure(error); return 0; } @@ -765,6 +806,15 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { int APS5_VABI flock_nid_postfix(int d, int operation) { if (GuestSockets::IsOpen(d)) return PosixFailure(GUEST_EOPNOTSUPP); File::RefuseArchivedFile(d, "flock"); +#ifdef _WIN32 + const auto handle = reinterpret_cast(WithoutParameterHandler([d] { return ::_get_osfhandle(d); })); + if (handle == INVALID_HANDLE_VALUE) return PosixFailure(GUEST_EBADF); + if (::GetFileType(handle) == FILE_TYPE_PIPE) return PosixFailure(GUEST_EOPNOTSUPP); +#else + struct stat status; + if (::fstat(d, &status) != 0) return PosixFailure(GUEST_EBADF); + if (S_ISFIFO(status.st_mode)) return PosixFailure(GUEST_EOPNOTSUPP); +#endif const int type = operation & 8 ? 8 : operation & 2 ? 2 : operation & 1 ? 1 : 0; if (type == 0) return PosixFailure(GUEST_EBADF); if (NativeFlock(d, type | (operation & 4)) != 0) { @@ -950,7 +1000,7 @@ int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb) { if (const int error = PathError(path)) return PosixFailure(error); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); - if (!File::FillLinkStat(native, sb)) return PosixResult(SceErrorFromErrno(errno)); + if (!File::FillLinkStat(native, sb)) return PosixResult(SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno)); return 0; } @@ -1332,6 +1382,8 @@ int APS5_VABI sceKernelRename(const char* from, const char* to) { if (!from || !to) 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) { @@ -1381,15 +1433,19 @@ int APS5_VABI fchmod_nid_postfix(int d, int mode) { return PosixResult(sceKernelFchmod(d, static_cast(mode))); } +static bool InvalidTimes(const KernelTimeval* times) { + return times != nullptr && (times[0].tv_usec < 0 || times[0].tv_usec >= 1000000 || times[1].tv_usec < 0 || times[1].tv_usec >= 1000000); +} + int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t length) { + if (length < 0) return SceErrorFromErrno(GUEST_EINVAL); if (!path) return SceErrorFromErrno(GUEST_EFAULT); if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); - if (length < 0) return SceErrorFromErrno(GUEST_EINVAL); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; const auto status = std::filesystem::status(native, error); - if (!std::filesystem::exists(status)) return SceErrorFromErrno(GUEST_ENOENT); + if (!std::filesystem::exists(status)) return SceErrorFromErrno(NotFoundError_nid_no_patch(native)); if (std::filesystem::is_directory(status)) return SceErrorFromErrno(GUEST_EISDIR); if (const int failure = NativeTruncate(native, length)) return SceErrorFromErrno(failure); RecordWrittenPath_nid_no_patch(native); @@ -1397,36 +1453,30 @@ int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t lengt } int APS5_VABI truncate_nid_postfix(const char* path, std::int64_t length) { + if (length < 0) return PosixFailure(GUEST_EINVAL); if (const int error = PathError(path)) return PosixFailure(error); return PosixResult(sceKernelTruncate_nid_postfix(path, length)); } int APS5_VABI sceKernelUtimes_nid_postfix(const char* path, const KernelTimeval* times) { + if (InvalidTimes(times)) return SCE_KERNEL_ERROR_EINVAL; if (!path) return SceErrorFromErrno(GUEST_EFAULT); if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); - if (times != nullptr) { - for (int i = 0; i < 2; ++i) { - if (times[i].tv_usec < 0 || times[i].tv_usec >= 1000000) return SCE_KERNEL_ERROR_EINVAL; - } - } std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); - if (NativeUtimes(native, times) != 0) return SceErrorFromErrno(errno); + if (NativeUtimes(native, times) != 0) return SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno); return 0; } int APS5_VABI utimes_nid_postfix(const char* path, const KernelTimeval* times) { + if (InvalidTimes(times)) return PosixFailure(GUEST_EINVAL); if (const int error = PathError(path)) return PosixFailure(error); return PosixResult(sceKernelUtimes_nid_postfix(path, times)); } int APS5_VABI lutimes_nid_postfix(const char* path, const KernelTimeval* times) { + if (InvalidTimes(times)) return PosixFailure(GUEST_EINVAL); if (const int error = PathError(path)) return PosixFailure(error); - if (times != nullptr) { - for (int i = 0; i < 2; ++i) { - if (times[i].tv_usec < 0 || times[i].tv_usec >= 1000000) return PosixFailure(GUEST_EINVAL); - } - } 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)); diff --git a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp index bf93cf4691..3a5d38dc3f 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp @@ -111,6 +111,13 @@ int APS5_VABI pthread_mutexattr_setprotocol_nid_postfix(PthreadMutexattr* attr, return 0; } +int APS5_VABI pthread_mutexattr_getprotocol_nid_postfix(const PthreadMutexattr* attr, int* protocol) { + if (!attr || !*attr) return PosixThread::GUEST_EINVAL; + if (!protocol) throw std::runtime_error("pthread_mutexattr_getprotocol: null protocol"); + *protocol = (*attr)->protocol; + return 0; +} + int APS5_VABI pthread_mutexattr_setprioceiling_nid_postfix(PthreadMutexattr* attr, int prioceiling) { if (!attr || !*attr) return PosixThread::GUEST_EINVAL; if ((*attr)->protocol != POSIX_PRIO_PROTECT) return POSIX_EINVAL; diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index 5846eb3464..03024d9d7f 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -169,6 +169,7 @@ bool GuestThreadCpuNanos(int cpuClockThread, std::uint64_t* nanos); void SetLiveGuestThreadsAffinity(KernelCpumask mask); KernelCpumask LiveGuestThreadsAffinity(); bool GuestThreadIsLive(const PthreadPrivate* thread); +std::unique_lock LockLiveGuestThread(const PthreadPrivate* thread); bool LiveGuestThreadExists(std::int64_t tid); bool GetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask& mask); bool SetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask mask); diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index 491a546c81..ea0eccba68 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -77,6 +77,8 @@ int APS5_VABI scePthreadAttrGetstack(const PthreadAttr* attr, void** stackaddr, int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr) { if (!thread || !attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + const auto live = LockLiveGuestThread(thread); + if (!live) return SCE_KERNEL_ERROR_ESRCH; (*attr)->_stacksize = thread->stackSize; (*attr)->stackAddress = thread->stackAddress; (*attr)->_detachstate = thread->_detached ? DETACH_DETACHED : DETACH_JOINABLE; diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 0afb764b67..bd61a61fc9 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -439,14 +439,17 @@ KernelCpumask LiveGuestThreadsAffinity() { return mask; } -bool GuestThreadIsLive(const PthreadPrivate* thread) { +std::unique_lock LockLiveGuestThread(const PthreadPrivate* thread) { auto& registry = CpuClocks(); - std::lock_guard lock(registry.lock); + std::unique_lock lock(registry.lock); for (const auto& [id, live] : registry.threads) { - if (live != thread) continue; - return !live->_finished.load(std::memory_order_acquire); + if (live == thread && !live->_finished.load(std::memory_order_acquire)) return lock; } - return false; + return {}; +} + +bool GuestThreadIsLive(const PthreadPrivate* thread) { + return LockLiveGuestThread(thread).owns_lock(); } bool GetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask& mask) { diff --git a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp index 8abae2ebcf..56bbe00efe 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp @@ -11,6 +11,7 @@ struct SocketOptionState { std::int64_t receiveTimeout = 0; }; +bool NativeListening(std::uintptr_t native, bool& listening); int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int option, const void* value, std::uint32_t length); int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int type, int option, void* value, std::uint32_t* length); int SetTcpLevelOption(std::uintptr_t native, bool tcp, int option, const void* value, std::uint32_t length); diff --git a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp index f40b09b5ad..37fce2af2e 100644 --- a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp +++ b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp @@ -7,6 +7,10 @@ #include #include #include +#ifdef __APPLE__ +#include +#include +#endif #endif #include "prx/libkernel/Socket/include/SocketError.hpp" #include "prx/libkernel/Socket/include/SocketOptions.hpp" @@ -160,6 +164,20 @@ int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int op return ErrorNoProtocolOption; } +bool NativeListening(std::uintptr_t native, bool& listening) { +#ifdef __APPLE__ + socket_fdinfo info{}; + if (proc_pidfdinfo(getpid(), static_cast(native), PROC_PIDFDSOCKETINFO, &info, sizeof(info)) != static_cast(sizeof(info))) return false; + listening = (info.psi.soi_options & SO_ACCEPTCONN) != 0; +#else + int accepting = 0; + socklen_t size = sizeof(accepting); + if (::getsockopt(static_cast(native), SOL_SOCKET, SO_ACCEPTCONN, reinterpret_cast(&accepting), &size)) return false; + listening = accepting != 0; +#endif + return true; +} + int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int type, int option, void* value, std::uint32_t* length) { const auto handle = static_cast(native); if (option == GuestLinger) { @@ -180,10 +198,9 @@ int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int ty if (::getsockopt(handle, SOL_SOCKET, nativeOption, reinterpret_cast(&enabled), &size)) return HostError(); number = enabled != 0 ? option : 0; } else if (option == GuestAcceptConnection) { - int listening = 0; - socklen_t size = sizeof(listening); - if (::getsockopt(handle, SOL_SOCKET, SO_ACCEPTCONN, reinterpret_cast(&listening), &size)) return HostError(); - number = listening != 0 ? GuestAcceptConnection : 0; + bool listening = false; + if (!NativeListening(native, listening)) return HostError(); + number = listening ? GuestAcceptConnection : 0; } else if (option == GuestType) { number = type; } else if (option == GuestNoSigpipe) { diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 718b58c187..7b64c96aae 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -69,13 +69,8 @@ int Fail(int error) { *__error_nid_postfix() = error; return -1; } constexpr int GuestPeek = 0x2; constexpr int GuestDontWait = 0x80; constexpr int GuestNoSignal = 0x20000; -#ifdef _WIN32 -constexpr int GuestSendFlags = GuestNoSignal; -constexpr int GuestReceiveFlags = GuestPeek; -#else constexpr int GuestSendFlags = GuestNoSignal | GuestDontWait; constexpr int GuestReceiveFlags = GuestPeek | GuestDontWait; -#endif int NativeSendFlags(int flags) { #ifdef _WIN32 static_cast(flags); @@ -228,12 +223,15 @@ std::shared_ptr Lookup(int descriptor) { std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); if (found != sockets.end()) return found->second; - Fail(9); + Fail(descriptor >= 0 && DescriptorIsOpen_nid_no_patch(descriptor) ? 38 : 9); return {}; } bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, socklen_t& size) { - if (!input || length < 2) { Fail(14); return false; } + if (length > 255) { Fail(63); return false; } + if (length < 2) { Fail(22); return false; } + if (!input) { Fail(14); return false; } const auto* bytes = static_cast(input); + if ((bytes[1] == 2 && length < 16) || (bytes[1] == 28 && length < 28)) { Fail(22); return false; } if (bytes[1] == 2 && length >= 16) { auto& v4 = reinterpret_cast(native); v4.sin_family = AF_INET; @@ -259,9 +257,8 @@ std::uint16_t& PortOf(sockaddr_storage& address) { return address.ss_family == AF_INET ? reinterpret_cast(address).sin_port : reinterpret_cast(address).sin6_port; } bool Listening(const Socket& socket) { - int listening = 0; - socklen_t size = sizeof(listening); - return ::getsockopt(socket.value, SOL_SOCKET, SO_ACCEPTCONN, reinterpret_cast(&listening), &size) == 0 && listening != 0; + bool listening = false; + return GuestSockets::NativeListening(static_cast(socket.value), listening) && listening; } bool ZeroPortConnect(const Socket& socket, const void* address, std::uint32_t length) { if (!address || (socket.type != 1 && socket.type != 2) || (socket.family != 2 && socket.family != 28)) return false; @@ -441,9 +438,7 @@ bool GuestSockets::Listening(int descriptor) { if (found == sockets.end()) return false; socket = found->second; } - int accepting = 0; - socklen_t length = sizeof(accepting); - return getsockopt(socket->value, SOL_SOCKET, SO_ACCEPTCONN, reinterpret_cast(&accepting), &length) == 0 && accepting != 0; + return Listening(*socket); } extern "C" bool GuestSocketIsOpen_nid_no_patch(int descriptor) { @@ -631,7 +626,16 @@ int SocketSendFlags(Socket& socket, int flags) { return NativeSendFlags(socket.options.noSignal ? flags | GuestNoSignal : flags); } int WaitForTransfer(Socket& socket, bool send, int flags) { - if ((flags & GuestDontWait) != 0) return 0; + if ((flags & GuestDontWait) != 0) { +#ifdef _WIN32 + NativePollDescriptor entry{}; + entry.fd = socket.value; + entry.events = static_cast(send ? POLLWRNORM : POLLRDNORM); + return WSAPoll(&entry, 1, 0) == 0 ? 35 : 0; +#else + return 0; +#endif + } std::int64_t timeout; { std::lock_guard lock(socket.modeMutex); @@ -1077,6 +1081,11 @@ int APS5_VABI socketpair_nid_postfix(int family, int type, int protocol, int* de GuestSockets::Close(firstDescriptor); return Fail(error); } + if ((type & GuestCloseOnExec) != 0) { + std::lock_guard registry(socketsMutex); + closeOnExecDescriptors.insert(firstDescriptor); + closeOnExecDescriptors.insert(secondDescriptor); + } descriptors[0] = firstDescriptor; descriptors[1] = secondDescriptor; return 0; @@ -1109,7 +1118,9 @@ int APS5_VABI listen_nid_postfix(int descriptor, int backlog) { const auto socket = Lookup(descriptor); if (!socket) return -1; if (socket->type != 1) return Fail(45); - return ::listen(socket->value, backlog) ? Fail(NativeError()) : 0; + if (::listen(socket->value, backlog) == 0) return 0; + const int error = NativeError(); + return Fail(error == 56 ? 22 : error); } int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* length) { const auto listener = Lookup(descriptor); @@ -1177,7 +1188,7 @@ int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_ } int APS5_VABI shutdown_nid_postfix(int descriptor, int how) { const auto socket = Lookup(descriptor); - if (!socket) return Fail(9); + if (!socket) return -1; if (how < 0 || how > 2) return Fail(22); if (::shutdown(socket->value, how)) return Fail(NativeError()); std::lock_guard lock(socket->modeMutex); @@ -1217,6 +1228,7 @@ int APS5_VABI getpeername_nid_postfix(int descriptor, void* address, std::uint32 } int APS5_VABI ioctl_nid_postfix(int descriptor, std::uint64_t request, void* argument) { if (!GuestSockets::IsOpen(descriptor)) return File::Ioctl(descriptor, request, argument); + if (File::IoctlRequestMalformed(request)) return Fail(25); const auto socket = Lookup(descriptor); if (!socket) return -1; if (!argument) return Fail(14); diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index 6dbef36c89..0f8f66a520 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -4,6 +4,8 @@ #include "prx/libc/include/General.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" +#include +#include #include #include #include @@ -12,6 +14,7 @@ #include #include #ifdef _WIN32 +#include #include #else #include @@ -123,9 +126,63 @@ void SetFpWord(GuestMcontext& m, std::size_t offset, std::uint32_t value) { std::memcpy(reinterpret_cast(m.fpstate) + offset, &value, sizeof(value)); } +char* AppendText(char* output, const char* text) { + while (*text != '\0') *output++ = *text++; + return output; +} + +char* AppendHex(char* output, std::uint64_t value, std::size_t digits) { + static constexpr char hexDigits[] = "0123456789abcdef"; + while (digits != 0) { + --digits; + *output++ = hexDigits[(value >> (digits * 4)) & 0xf]; + } + return output; +} + +char* AppendHexField(char* output, const char* name, std::uint64_t value, std::size_t digits) { + return AppendHex(AppendText(output, name), value, digits); +} + +void LogGuestExceptionContext(int signum, const GuestMcontext& context) { + char buffer[768]; + char* output = AppendText(buffer, "[exception] guest context"); + output = AppendHexField(output, " signal=0x", static_cast(signum), 2); + output = AppendHexField(output, " rip=0x", context.rip, 16); + output = AppendHexField(output, " rsp=0x", context.rsp, 16); + output = AppendHexField(output, " rflags=0x", context.rflags, 16); + output = AppendHexField(output, " trap=0x", context.trapno, 8); + output = AppendHexField(output, " error=0x", context.err, 16); + output = AppendHexField(output, " fault=0x", context.addr, 16); + output = AppendHexField(output, " rax=0x", context.rax, 16); + output = AppendHexField(output, " rbx=0x", context.rbx, 16); + output = AppendHexField(output, " rcx=0x", context.rcx, 16); + output = AppendHexField(output, " rdx=0x", context.rdx, 16); + output = AppendHexField(output, " rsi=0x", context.rsi, 16); + output = AppendHexField(output, " rdi=0x", context.rdi, 16); + output = AppendHexField(output, " rbp=0x", context.rbp, 16); + output = AppendHexField(output, " r8=0x", context.r8, 16); + output = AppendHexField(output, " r9=0x", context.r9, 16); + output = AppendHexField(output, " r10=0x", context.r10, 16); + output = AppendHexField(output, " r11=0x", context.r11, 16); + output = AppendHexField(output, " r12=0x", context.r12, 16); + output = AppendHexField(output, " r13=0x", context.r13, 16); + output = AppendHexField(output, " r14=0x", context.r14, 16); + output = AppendHexField(output, " r15=0x", context.r15, 16); + *output++ = '\n'; +#ifdef _WIN32 + DWORD written = 0; + WriteFile(GetStdHandle(STD_ERROR_HANDLE), buffer, static_cast(output - buffer), &written, nullptr); +#else + const auto written = write(STDERR_FILENO, buffer, static_cast(output - buffer)); + (void)written; +#endif +} + void CallHandler(GuestExceptionHandler handler, int signum, GuestUcontext& ucontext) { auto& m = ucontext.mcontext; const std::uint32_t mxcsrMask = FpWord(m, MxcsrMaskOffset); + LogGuestExceptionContext(signum, m); handler(signum, &ucontext); SetFpWord(m, MxcsrOffset, FpWord(m, MxcsrOffset) & (mxcsrMask != 0 ? mxcsrMask : DefaultMxcsrMask)); SetFpWord(m, MxcsrMaskOffset, mxcsrMask); @@ -249,6 +306,83 @@ bool InWinpthread(DWORD64 rip) { return winpthread != nullptr && VirtualQuery(reinterpret_cast(rip), &info, sizeof(info)) == sizeof(info) && info.AllocationBase == winpthread; } +enum class CodeKind : std::uint8_t { Unknown, Guest, Host }; + +struct ImageKinds { + static constexpr std::size_t Capacity = 256; + std::array, Capacity> bases{}; + std::array, Capacity> kinds{}; + std::atomic count{0}; +}; + +ImageKinds& Images() { + static ImageKinds images; + return images; +} + +DWORD64 ImageBase(DWORD64 rip) { + MEMORY_BASIC_INFORMATION info{}; + if (VirtualQuery(reinterpret_cast(rip), &info, sizeof(info)) != sizeof(info) || info.Type != MEM_IMAGE) return 0; + return reinterpret_cast(info.AllocationBase); +} + +CodeKind CachedKind(DWORD64 base) { + if (base == 0) return CodeKind::Guest; + auto& images = Images(); + const auto count = std::min(images.count.load(std::memory_order_acquire), ImageKinds::Capacity); + for (std::size_t index = 0; index < count; ++index) { + if (images.bases[index].load(std::memory_order_acquire) == base) return static_cast(images.kinds[index].load(std::memory_order_acquire)); + } + return CodeKind::Unknown; +} + +DWORD64 ExecutableBase() { + static const auto executable = reinterpret_cast(GetModuleHandleW(nullptr)); + return executable; +} + +CodeKind ClassifyImage(DWORD64 base) { + if (const auto known = CachedKind(base); known != CodeKind::Unknown) return known; + auto kind = CodeKind::Host; + if (base == ExecutableBase()) { + kind = CodeKind::Guest; + } else { + wchar_t path[MAX_PATH]{}; + const auto length = GetModuleFileNameW(reinterpret_cast(base), path, MAX_PATH); + const std::wstring_view name(path, length); + constexpr std::wstring_view guestSuffix = L".guest.prx"; + if (name.size() >= guestSuffix.size() && name.substr(name.size() - guestSuffix.size()) == guestSuffix) kind = CodeKind::Guest; + } + auto& images = Images(); + if (const auto slot = images.count.fetch_add(1, std::memory_order_acq_rel); slot < ImageKinds::Capacity) { + images.kinds[slot].store(static_cast(kind), std::memory_order_release); + images.bases[slot].store(base, std::memory_order_release); + } + return kind; +} + +bool BlockedInWait(DWORD64 rip) { + static const std::vector waits = [] { + std::vector stubs; + const HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); + for (const char* name : {"NtWaitForSingleObject", "NtWaitForMultipleObjects", "NtWaitForAlertByThreadId", "NtDelayExecution", "NtRemoveIoCompletion", "NtRemoveIoCompletionEx", "NtWaitForKeyedEvent", "NtSignalAndWaitForSingleObject", "NtWaitForWorkViaWorkerFactory"}) { + if (const auto address = reinterpret_cast(GetProcAddress(ntdll, name)); address != 0) stubs.push_back(address); + } + if (const HMODULE win32u = GetModuleHandleW(L"win32u.dll"); win32u != nullptr) { + if (const auto address = reinterpret_cast(GetProcAddress(win32u, "NtUserMsgWaitForMultipleObjectsEx")); address != 0) stubs.push_back(address); + } + return stubs; + }(); + return std::any_of(waits.begin(), waits.end(), [rip](DWORD64 stub) { return rip - stub < 0x20; }); +} + +CodeKind RunningCode(DWORD64 rip, DWORD64& base) { + base = ImageBase(rip); + auto kind = CachedKind(base); + if (kind == CodeKind::Unknown && Images().count.load(std::memory_order_acquire) >= ImageKinds::Capacity) kind = CodeKind::Host; + return kind == CodeKind::Host && BlockedInWait(rip) ? CodeKind::Guest : kind; +} + bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { if (thread == scePthreadSelf()) { CONTEXT context{}; @@ -260,6 +394,11 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { auto queued = std::make_unique(Delivery{handler, signum, {}, {}, 0}); alignas(16) Delivery delivery{handler, signum, {}, {}, SaveVectors ? 1u : 0u}; const auto retryDeadline = std::chrono::steady_clock::now() + RetryLimit; + static_cast(RestoringContext(0)); + static_cast(InWinpthread(0)); + static_cast(BlockedInWait(0)); + static_cast(ExecutableBase()); + static_cast(Images()); for (;;) { if (SuspendThread(native) == static_cast(-1)) { if (Exited(native)) return false; @@ -281,9 +420,16 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { queued.release(); return true; } - if (!RestoringContext(delivery.context.Rip) && !InWinpthread(delivery.context.Rip)) break; + const bool restoring = RestoringContext(delivery.context.Rip) || InWinpthread(delivery.context.Rip); + DWORD64 image = 0; + const auto running = restoring ? CodeKind::Guest : RunningCode(delivery.context.Rip, image); + if (!restoring && (running == CodeKind::Guest || (running == CodeKind::Host && std::chrono::steady_clock::now() >= retryDeadline))) break; ResumeThread(native); - if (std::chrono::steady_clock::now() >= retryDeadline) throw std::runtime_error("sceKernelRaiseException: the target thread stayed inside NtContinue or winpthreads for 1 s"); + if (running == CodeKind::Unknown) { + static_cast(ClassifyImage(image)); + continue; + } + if (restoring && std::chrono::steady_clock::now() >= retryDeadline) throw std::runtime_error("sceKernelRaiseException: the target thread stayed inside NtContinue or winpthreads for 1 s"); SwitchToThread(); } const DWORD64 slot = (delivery.context.Rsp - RedZone - sizeof(Delivery)) & ~static_cast(15); @@ -515,11 +661,11 @@ int APS5_VABI sceKernelRaiseException(Pthread thread, int signum) { } void APS5_VABI sceKernelDebugRaiseException(int c1, int c2) { - APS5_LOG_OUT("sceKernelDebugRaiseException c1=%d c2=%d", c1, c2); + APS5_LOG_OUT("sceKernelDebugRaiseException c1=0x%08x c2=0x%08x caller=%p", static_cast(c1), static_cast(c2), __builtin_return_address(0)); } void APS5_VABI sceKernelDebugRaiseExceptionOnReleaseMode(int c1, int c2) { - APS5_LOG_OUT("sceKernelDebugRaiseExceptionOnReleaseMode c1=%d c2=%d", c1, c2); + APS5_LOG_OUT("sceKernelDebugRaiseExceptionOnReleaseMode c1=0x%08x c2=0x%08x caller=%p", static_cast(c1), static_cast(c2), __builtin_return_address(0)); } } diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 4f100c3da7..d9edcad564 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -736,6 +736,12 @@ int APS5_VABI setuid_nid_postfix(std::uint32_t uid) { return 0; } +int APS5_VABI setgid_nid_postfix(std::uint32_t gid) { + if (gid != 0) + throw std::runtime_error("setgid: unsupported group id " + std::to_string(gid)); + return 0; +} + int APS5_VABI setreuid_nid_postfix(std::uint32_t ruid, std::uint32_t euid) { constexpr std::uint32_t unchanged = 0xffffffffu; if (ruid != 0 && ruid != unchanged) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index dadee5d0d5..8c73a85624 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -5,6 +5,7 @@ #include "prx/libkernel/System/include/HostCpu.hpp" #include "SceTypes.hpp" #include +#include #include #include #include @@ -74,9 +75,24 @@ const SystemString* FindSystemString(const int* name) { } bool IsLeafMib(const int* name) { - return (name[0] == 6 && (name[1] == 2 || name[1] == 3 || name[1] == 5 || name[1] == 7)) || (name[0] == 1 && name[1] == 37); + return (name[0] == 6 && (name[1] == 2 || name[1] == 3 || name[1] == 5 || name[1] == 7)) || (name[0] == 1 && (name[1] == 37 || name[1] == 50)); } +bool IsMeasuredAbsentMib(const int* name, std::uint32_t nameLength) { + static constexpr struct { + int mib[3]; + std::uint32_t length; + } absent[] = { + {{1, 2147482816, 2147482814}, 3}, + {{1, 2147483151}, 2}, + {{1, 2147483182, 2147483181}, 3}, + {{1, 2147482838, 2147482834}, 3}, + }; + for (const auto& entry : absent) { + if (nameLength == entry.length && std::memcmp(name, entry.mib, entry.length * sizeof(int)) == 0) return true; + } + return false; +} constexpr std::int32_t VmEntryDefaultType = 1; struct GuestVmEntry { @@ -186,6 +202,11 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *__error_nid_postfix() = 14; return -1; } + if (IsMeasuredAbsentMib(name, nameLength)) { + if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": writing an unmeasured absent MIB"); + *__error_nid_postfix() = 2; + return -1; + } if (nameLength == 2 && name[0] == 6 && name[1] == 7) { if (newValue != nullptr) { *__error_nid_postfix() = 1; @@ -280,6 +301,15 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *oldLength = count; return 0; } + if (nameLength == 2 && name[0] == 1 && name[1] == 50) { + if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": writing kern.rng_pseudo is unverified"); + std::array bytes{}; + if (oldValue != nullptr && oldLength != nullptr) { + static std::random_device device; + for (auto& byte : bytes) byte = static_cast(device()); + } + return CopyOut(bytes.data(), bytes.size(), oldValue, oldLength); + } if (nameLength > 2 && (IsLeafMib(name) || FindSystemString(name) != nullptr)) { *__error_nid_postfix() = 20; return -1; @@ -314,11 +344,11 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si *__error_nid_postfix() = 1; return -1; } - if (oldValue == nullptr || oldLength == nullptr) throw std::runtime_error(std::string(__func__) + ": kern.rng_pseudo without an output buffer"); static std::random_device device; - auto* bytes = static_cast(oldValue); - for (std::size_t i = 0; i < *oldLength; ++i) bytes[i] = static_cast(device()); - return 0; + std::uint8_t block[64]{}; + if (oldValue != nullptr && oldLength != nullptr) + for (auto& byte : block) byte = static_cast(device()); + return CopyOut(block, sizeof(block), oldValue, oldLength); } if (std::strcmp(name, "vm.overcommit") == 0) { if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": setting vm.overcommit"); @@ -387,6 +417,7 @@ int APS5_VABI sysctlnametomib_nid_postfix(const char* name, int* mib, std::size_ {"hw.physmem", {6, 5}, 2}, {"hw.realmem", {6, 12}, 2}, {"kern.arandom", {1, 37}, 2}, + {"kern.rng_pseudo", {1, 50}, 2}, {"kern.proc.pathname", {1, 14, 12}, 3}, }; constexpr std::size_t MaximumPath = 1024; diff --git a/core/libs/tests/GuestAccess.cpp b/core/libs/tests/GuestAccess.cpp index 0cce786ad5..f4fa4fb8e8 100644 --- a/core/libs/tests/GuestAccess.cpp +++ b/core/libs/tests/GuestAccess.cpp @@ -38,6 +38,10 @@ int main() { Require(access_nid_postfix(nullptr, 0) == -1 && *__error_nid_postfix() == 14); Require(access_nid_postfix("sample.txt", 8) == -1 && *__error_nid_postfix() == 22); Require(access_nid_postfix("sample.txt", -1) == -1 && *__error_nid_postfix() == 22); + Require(access_nid_postfix(nullptr, 8) == -1 && *__error_nid_postfix() == 22); + Require(access_nid_postfix(nullptr, -1) == -1 && *__error_nid_postfix() == 22); + Require(access_nid_postfix("", 8) == -1 && *__error_nid_postfix() == 22); + Require(access_nid_postfix("", -1) == -1 && *__error_nid_postfix() == 22); #ifdef _WIN32 Require(SetFileAttributesW(file.c_str(), FILE_ATTRIBUTE_READONLY)); Require(access_nid_postfix("sample.txt", 4) == 0); diff --git a/core/libs/tests/GuestAcm.cpp b/core/libs/tests/GuestAcm.cpp index e360f84445..c6691f898d 100644 --- a/core/libs/tests/GuestAcm.cpp +++ b/core/libs/tests/GuestAcm.cpp @@ -18,24 +18,49 @@ static void Require(bool value) { if (!value) std::abort(); } -int main() { - std::uint32_t context = 0; - Require(sceAcmContextCreate(nullptr) != 0); - Require(sceAcmContextCreate(&context) == 0 && context != 0); +constexpr int BadContext = static_cast(0x81940002); +constexpr int BadPointer = static_cast(0x81940006); - std::uint32_t batch = 0; - Require(sceAcmBatchStartBuffer(context, nullptr, 0, nullptr, nullptr) != 0); - Require(sceAcmBatchStartBuffer(context, nullptr, 0, nullptr, &batch) == 0 && batch != 0); - Require(sceAcmBatchWait(context, batch, 0) == 0); +int main() { + Require(sceAcmContextCreate(nullptr) == BadPointer); + Require(sceAcmContextDestroy(0) == BadContext); + Require(sceAcmContextDestroy(999999) == BadContext); + + std::uint32_t context1 = 0; + Require(sceAcmContextCreate(&context1) == 0 && context1 != 0); + + std::uint32_t context2 = 0; + Require(sceAcmContextCreate(&context2) == 0 && context2 != 0 && context2 != context1); + + std::uint32_t batch1 = 0; + Require(sceAcmBatchStartBuffer(0, nullptr, 0, nullptr, &batch1) == BadContext); + Require(sceAcmBatchStartBuffer(999999, nullptr, 0, nullptr, &batch1) == BadContext); + Require(sceAcmBatchStartBuffer(context1, nullptr, 0, nullptr, nullptr) == BadPointer); + Require(sceAcmBatchStartBuffer(context1, nullptr, 0, nullptr, &batch1) == 0 && batch1 != 0); + + Require(sceAcmBatchWait(0, batch1, 0) == BadContext); + Require(sceAcmBatchWait(999999, batch1, 0) == BadContext); + Require(sceAcmBatchWait(context1, batch1, 0) == 0); + Require(sceAcmBatchWait(context1, batch1, 0) == 0); std::uint32_t batch2 = 0; - Require(sceAcmBatchStartBuffers(context, 0, nullptr, nullptr, nullptr) != 0); - Require(sceAcmBatchStartBuffers(context, 0, nullptr, nullptr, &batch2) == 0 && batch2 != 0); - Require(sceAcmBatchWait(context, batch2, 0) == 0); + Require(sceAcmBatchStartBuffers(0, 0, nullptr, nullptr, &batch2) == BadContext); + Require(sceAcmBatchStartBuffers(999999, 0, nullptr, nullptr, &batch2) == BadContext); + Require(sceAcmBatchStartBuffers(context2, 0, nullptr, nullptr, nullptr) == BadPointer); + Require(sceAcmBatchStartBuffers(context2, 0, nullptr, nullptr, &batch2) == 0 && batch2 != 0); + Require(sceAcmBatchWait(context2, batch2, 0) == 0); Require(sceAcm_ConvReverb_SharedInput() == 0); Require(sceAcm_FFT() == 0); Require(sceAcm_Panner() == 0); - Require(sceAcmContextDestroy(context) == 0); + Require(sceAcmContextDestroy(context1) == 0); + Require(sceAcmContextDestroy(context1) == BadContext); + Require(sceAcmBatchStartBuffer(context1, nullptr, 0, nullptr, &batch1) == BadContext); + Require(sceAcmBatchWait(context1, batch1, 0) == BadContext); + + Require(sceAcmContextDestroy(context2) == 0); + Require(sceAcmContextDestroy(context2) == BadContext); + + return 0; } diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 8053aae794..017b6304a8 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -833,6 +833,37 @@ void TestBatchCancel(std::uint32_t context) { Require(sceAjmBatchCancel(context, id + 1) == invalidBatch); } +void TestBatchContextOwnership(std::uint32_t context) { + constexpr int invalidContext = static_cast(0x80930002); + constexpr int invalidBatch = static_cast(0x80930004); + std::uint32_t other = 0; + Require(sceAjmInitialize(0, &other) == 0 && other != context); + std::vector storage(256); + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(storage.data(), storage.size(), &info) == 0); + AjmBatchError error{}; + std::uint32_t first = 0; + std::uint32_t second = 0; + Require(sceAjmBatchStart(context, &info, 0, &error, &first) == 0); + Require(sceAjmBatchStart(other, &info, 0, &error, &second) == 0); + Require(sceAjmBatchCancel(other, first) == invalidBatch); + Require(sceAjmBatchWait(other, first, 0, &error) == invalidBatch); + Require(sceAjmBatchCancel(context, second) == invalidBatch); + Require(sceAjmBatchWait(context, second, 0, &error) == invalidBatch); + Require(sceAjmBatchWait(0, first, 0, &error) == invalidContext); + Require(sceAjmBatchWait(other + 1, first, 0, &error) == invalidContext); + Require(sceAjmBatchCancel(context, first) == 0); + Require(sceAjmBatchWait(context, first, 0, &error) == 0); + Require(sceAjmBatchWait(other, second, 0, &error) == 0); + Require(sceAjmBatchWait(context, first, 0, &error) == invalidBatch); + Require(sceAjmBatchWait(other, second, 0, &error) == invalidBatch); + std::uint32_t untouched = 0xfeedu; + Require(sceAjmBatchStart(0, &info, 0, &error, &untouched) == invalidContext); + Require(sceAjmBatchStart(other + 1, &info, 0, &error, &untouched) == invalidContext); + Require(untouched == 0xfeedu); + Require(sceAjmFinalize(other) == 0); +} + constexpr std::uint64_t CONTROL_RESET = 1ull << 13; constexpr std::uint64_t CONTROL_INITIALIZE = 1ull << 14; constexpr std::uint64_t SIDEBAND_GAPLESS_DECODE = 1ull << 45; @@ -1928,6 +1959,7 @@ int main() { TestBatchWaitRelease(context); TestInstanceHandleLifetime(context); TestBatchCancel(context); + TestBatchContextOwnership(context); TestControlAt9(context); TestMultichannelAt9(context); TestControlMp3(context); diff --git a/core/libs/tests/GuestAmprThreads.cpp b/core/libs/tests/GuestAmprThreads.cpp index 264ff62a6d..0c669decd2 100644 --- a/core/libs/tests/GuestAmprThreads.cpp +++ b/core/libs/tests/GuestAmprThreads.cpp @@ -1,6 +1,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/exceptions/Runtime.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -188,6 +189,23 @@ static void TestThreadSleep() { Require(_Thrd_sleep_nid_postfix(&negative, nullptr) == 0); Require(RealtimeNanos() >= borrowed.sec * NanosPerSecond + borrowed.nsec); +#ifdef _WIN32 + for (const auto duration : {100000LL, 2000000LL}) { + std::array samples{}; + for (auto& sample : samples) { + const auto start = std::chrono::steady_clock::now(); + const auto shortTarget = XtimeAfter(duration); + Require(_Thrd_sleep_nid_postfix(&shortTarget, nullptr) == ThrdSuccess); + sample = std::chrono::steady_clock::now() - start; + Require(RealtimeNanos() >= shortTarget.sec * NanosPerSecond + shortTarget.nsec); + } + std::sort(samples.begin(), samples.end()); + const auto median = samples[samples.size() / 2]; + std::fprintf(stderr, "_Thrd_sleep(%lld ns) median: %lld ns\n", duration, static_cast(median.count())); + Require(median < std::chrono::milliseconds(12)); + } +#endif + bool rejected = false; try { _Thrd_sleep_nid_postfix(nullptr, nullptr); diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index fef723221e..6d3745cd70 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -51,6 +51,9 @@ int main() { Require(sceAppContentDownloadDataGetAvailableSpaceKb(&download, &availableKb) == 0); Require(availableKb == 0); Require(sceAppContentDownloadDataGetAvailableSpaceKb(&download, nullptr) == ErrorParameter); + Require(sceAppContentDownloadDataGetAvailableSpaceKb(nullptr, &availableKb) == ErrorParameter); + AppContentMountPoint invalidMount{}; + Require(sceAppContentDownloadDataGetAvailableSpaceKb(&invalidMount, &availableKb) == ErrorNotMounted); const auto downloadData = ResolvePath_nid_no_patch("/download0"); Require(sceAppContentDownloadDataFormat(&download) == 0); Require(std::filesystem::is_directory(downloadData) && std::filesystem::is_empty(downloadData)); @@ -58,6 +61,7 @@ int main() { { std::ofstream file(downloadData / "cache" / "old.bin"); file << "downloaded"; } { std::ofstream file(downloadData / "index.dat"); file << "downloaded"; } Require(sceAppContentDownloadDataFormat(nullptr) == ErrorParameter); + Require(sceAppContentDownloadDataFormat(&invalidMount) == ErrorNotMounted); Require(std::filesystem::exists(downloadData / "index.dat")); Require(sceAppContentDownloadDataFormat(&download) == 0); Require(std::filesystem::is_directory(downloadData) && std::filesystem::is_empty(downloadData)); diff --git a/core/libs/tests/GuestAudioIn.cpp b/core/libs/tests/GuestAudioIn.cpp index 7dc3e6ff92..124bc9a880 100644 --- a/core/libs/tests/GuestAudioIn.cpp +++ b/core/libs/tests/GuestAudioIn.cpp @@ -17,6 +17,8 @@ int APS5_VABI sceAudioInOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, s int APS5_VABI sceAudioInHqOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI sceAudioInAsyncOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); int32_t APS5_VABI sceAudioInIsPadEmbeddedMic(int32_t); +int32_t APS5_VABI sceAudioInGetGain(int32_t, float*); +int32_t APS5_VABI sceAudioInGetRerouteCount(int32_t); int APS5_VABI sceAudioInInput(int, void*); int APS5_VABI sceAudioInGetSilentState(int); int APS5_VABI sceAudioInClose(int); @@ -34,6 +36,16 @@ static bool ReportsNotImplemented(const char* name, TCall call) { return false; } +template +static bool ThrowsRuntimeError(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + static void SetEnvironment(const char* name, const std::string& value) { #ifdef _WIN32 _putenv_s(name, value.c_str()); @@ -47,6 +59,7 @@ namespace { constexpr int user = 0x10000000; void TestValidation() { + constexpr int invalidHandle = static_cast(0x80260101); constexpr int invalidSize = static_cast(0x80260102); constexpr int invalidFreq = static_cast(0x80260103); constexpr int invalidType = static_cast(0x80260104); @@ -70,14 +83,28 @@ void TestValidation() { Require(sceAudioInAsyncOpen(user, 0, 0, 256, 48000, 0) == invalidParam); Require(sceAudioInHqOpen(user, 0, 0, 256, 48000, 2) == invalidSize); Require(sceAudioInHqOpen(user, 0, 0, 128, 16000, 2) == invalidFreq); + for (const int badHandle : {-1, 0, 1, 9}) { + float gain = -2.5f; + Require(sceAudioInGetGain(badHandle, &gain) == invalidHandle && gain == 1.0f); + Require(sceAudioInGetRerouteCount(badHandle) == invalidHandle); + Require(ThrowsRuntimeError([badHandle] { sceAudioInGetGain(badHandle, nullptr); })); + } int port = sceAudioInAsyncOpen(user, 0, 0, 384, 48000, 2); - Require(port > 0 && sceAudioInClose(port) == 0); + float gain = 0.0f; + Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0); + Require(ThrowsRuntimeError([port] { sceAudioInGetGain(port, nullptr); })); + Require(sceAudioInClose(port) == 0); + gain = 0.25f; + Require(sceAudioInGetGain(port, &gain) == invalidHandle && gain == 1.0f && sceAudioInGetRerouteCount(port) == invalidHandle); port = sceAudioInOpen(user, 0, 0, 256, 48000, 0x10); - Require(port > 0 && sceAudioInClose(port) == 0); + gain = -1.0f; + Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0 && sceAudioInClose(port) == 0); port = sceAudioInHqOpen(user, 0, 0, 128, 48000, 0x10); - Require(port > 0 && sceAudioInClose(port) == 0); + gain = 2.0f; + Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0 && sceAudioInClose(port) == 0); port = sceAudioInAsyncOpen(user, 0, 0, 384, 48000, 0x10); - Require(port > 0 && sceAudioInClose(port) == 0); + gain = 0.5f; + Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0 && sceAudioInClose(port) == 0); Require(ReportsNotImplemented("sceAudioInIsPadEmbeddedMic", [] { sceAudioInIsPadEmbeddedMic(1); })); } diff --git a/core/libs/tests/GuestBessel.cpp b/core/libs/tests/GuestBessel.cpp new file mode 100644 index 0000000000..75c94ec4ac --- /dev/null +++ b/core/libs/tests/GuestBessel.cpp @@ -0,0 +1,187 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +double APS5_VABI j0_nid_postfix(double); +double APS5_VABI j1_nid_postfix(double); +double APS5_VABI jn_nid_postfix(int, double); +double APS5_VABI y0_nid_postfix(double); +double APS5_VABI y1_nid_postfix(double); +double APS5_VABI yn_nid_postfix(int, double); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static double Value(std::uint64_t bits) { return std::bit_cast(bits); } + +static std::uint64_t Bits(double value) { return std::bit_cast(value); } + +struct UnaryCase { std::uint64_t input; std::uint64_t output; }; +struct OrderCase { int order; std::uint64_t input; std::uint64_t output; }; + +static void CheckUnary(const char* name, double (APS5_VABI *function)(double), const UnaryCase* cases, std::size_t count) { + for (std::size_t index = 0; index < count; ++index) { + *__error_nid_postfix() = 12345; + const double result = function(Value(cases[index].input)); + if (Bits(result) != cases[index].output || *__error_nid_postfix() != 12345) { + std::fprintf(stderr, "Guest %s failed for %016llx: %016llx\n", name, static_cast(cases[index].input), + static_cast(Bits(result))); + std::abort(); + } + } +} + +static void CheckOrder(const char* name, double (APS5_VABI *function)(int, double), const OrderCase* cases, std::size_t count) { + for (std::size_t index = 0; index < count; ++index) { + *__error_nid_postfix() = 12345; + const double result = function(cases[index].order, Value(cases[index].input)); + if (Bits(result) != cases[index].output || *__error_nid_postfix() != 12345) { + std::fprintf(stderr, "Guest %s failed for %d, %016llx: %016llx\n", name, cases[index].order, + static_cast(cases[index].input), static_cast(Bits(result))); + std::abort(); + } + } +} + +static void CheckFirstKind() { + const UnaryCase zero[] = { + {0x0000000000000000ull, 0x3ff0000000000000ull}, {0x8000000000000000ull, 0x3ff0000000000000ull}, {0x3ddb7cdfd9d7bdbbull, 0x3ff0000000000000ull}, + {0x3e40000000000000ull, 0x3ff0000000000000ull}, {0x3eb0000000000000ull, 0x3feffffffffff800ull}, {0x3f1fffffffffffffull, 0x3feffffffe000000ull}, + {0x3f20000000000000ull, 0x3feffffffe000000ull}, {0x3fd0000000000000ull, 0x3fef807fc72aa864ull}, {0xbfe0000000000000ull, 0x3fee07f1d54c3f34ull}, + {0x3fe8000000000000ull, 0x3feba7df6a752a19ull}, {0x3fefffffffffffffull, 0x3fe87c7fdbd7b8f0ull}, {0x3ff0000000000000ull, 0x3fe87c7fdbd7b8f0ull}, + {0xbff8000000000000ull, 0x3fe060e46ce9651bull}, {0x3fffffffffffffffull, 0x3fcca873fb24cefcull}, {0x7ff0000000000000ull, 0x0000000000000000ull}, + {0xfff0000000000000ull, 0x0000000000000000ull}, {0x7ff8000000000123ull, 0x7ff8000000000123ull}, {0x3fee4317b35275e1ull, 0x3fe93c4854d71c27ull}, + {0x3fe7084d7429a0caull, 0x3febfce08c913631ull}, {0x3ff896d4ffdc2a13ull, 0x3fdf6faac7e7d3ceull}, {0x3ff28d079588dbb8ull, 0x3fe61dc352e1dc33ull}, + {0x3ffe7d3678a4d9a8ull, 0x3fd1d44a54769967ull}, + }; + CheckUnary("j0", j0_nid_postfix, zero, std::size(zero)); + const UnaryCase one[] = { + {0x0000000000000000ull, 0x0000000000000000ull}, {0x8000000000000000ull, 0x8000000000000000ull}, {0x3ddb7cdfd9d7bdbbull, 0x3dcb7cdfd9d7bdbbull}, + {0x3e40000000000000ull, 0x3e30000000000000ull}, {0x3eb0000000000000ull, 0x3e9ffffffffffc00ull}, {0x3f1fffffffffffffull, 0x3f0ffffffeffffffull}, + {0x3f20000000000000ull, 0x3f0fffffff000000ull}, {0x3fd0000000000000ull, 0x3fbfc02a9c749ee9ull}, {0xbfe0000000000000ull, 0xbfcf02a71f4870d7ull}, + {0x3fe8000000000000ull, 0x3fd65a01d66b68bdull}, {0x3fefffffffffffffull, 0x3fdc29c9ee970c6cull}, {0x3ff0000000000000ull, 0x3fdc29c9ee970c6cull}, + {0xbff8000000000000ull, 0xbfe1da9da9d6fc81ull}, {0x3fffffffffffffffull, 0x3fe27487958371f0ull}, {0x7ff0000000000000ull, 0x0000000000000000ull}, + {0xfff0000000000000ull, 0x8000000000000000ull}, {0x7ff8000000000123ull, 0x7ff8000000000123ull}, {0x3fecd8b073b795c0ull, 0x3fda038c68b70948ull}, + {0x3fffe99bfa2fcbc2ull, 0x3fe2775e2433b1d8ull}, {0x3ffa917f997cc231ull, 0x3fe267a06350cc21ull}, + }; + CheckUnary("j1", j1_nid_postfix, one, std::size(one)); +} + +static void CheckSecondKind() { + const UnaryCase zero[] = { + {0x0000000000000000ull, 0xfff0000000000000ull}, {0x8000000000000000ull, 0xfff0000000000000ull}, {0xbff0000000000000ull, 0x7ff8000000000000ull}, + {0x8000000000000001ull, 0x7ff8000000000000ull}, {0x7ff0000000000000ull, 0x0000000000000000ull}, + }; + CheckUnary("y0", y0_nid_postfix, zero, std::size(zero)); + const UnaryCase one[] = { + {0x0000000000000000ull, 0xfff0000000000000ull}, {0x8000000000000000ull, 0xfff0000000000000ull}, {0xbff0000000000000ull, 0x7ff8000000000000ull}, + {0x8000000000000001ull, 0x7ff8000000000000ull}, {0x7ff0000000000000ull, 0x0000000000000000ull}, {0x3bc79ca10c924223ull, 0xc40b9be638027fc9ull}, + {0x3c90000000000000ull, 0xc3445f306dc9c883ull}, {0x0000000000000001ull, 0xfff0000000000000ull}, + }; + CheckUnary("y1", y1_nid_postfix, one, std::size(one)); + Require(std::isnan(y0_nid_postfix(-std::numeric_limits::infinity())) && std::isnan(y1_nid_postfix(-std::numeric_limits::infinity()))); + Require(std::isnan(y0_nid_postfix(std::numeric_limits::quiet_NaN())) && std::isnan(y1_nid_postfix(std::numeric_limits::quiet_NaN()))); + for (const double input : {0.0, -0.0, -1.0, -std::numeric_limits::denorm_min()}) { + std::feclearexcept(FE_ALL_EXCEPT); + y0_nid_postfix(input); + y1_nid_postfix(input); + yn_nid_postfix(3, input); + Require(std::fetestexcept(FE_DIVBYZERO | FE_INVALID) == 0); + } +} + +static void CheckIntegerOrders() { + const OrderCase first[] = { + {2, 0x3ddb7cdfd9d7bdbbull, 0x3b979ca10c924224ull}, {5, 0xbddb7cdfd9d7bdbbull, 0xb4cfed4f3ec0c466ull}, {33, 0x3e00000000000000ull, 0x0000000000000000ull}, + {34, 0x3e00000000000000ull, 0x0000000000000000ull}, {-3, 0x3e00000000000000ull, 0xb9c5555555555555ull}, {2, 0x3ff8000000000000ull, 0x3fcdb50c80d5039aull}, + {5, 0x3ff0000000000000ull, 0x3f305e3d487e8377ull}, {10, 0x3fe0000000000000ull, 0x3d52637b5990470aull}, {-3, 0x3ff4000000000000ull, 0xbfa2e068dde7f417ull}, + {3, 0xbff4000000000000ull, 0xbfa2e068dde7f417ull}, {4, 0xbff4000000000000ull, 0x3f781279da92ee26ull}, {40, 0x3ff8000000000000ull, 0x34f2a1a3311d8462ull}, + {300, 0x3ff0000000000000ull, 0x0000000000000000ull}, {3, 0x8000000000000000ull, 0x8000000000000000ull}, {3, 0xfff0000000000000ull, 0x8000000000000000ull}, + {2, 0xfff0000000000000ull, 0x0000000000000000ull}, {7, 0x7ff8000000000123ull, 0x7ff8000000000123ull}, {-1, 0x3fe0000000000000ull, 0xbfcf02a71f4870d7ull}, + {0, 0xbfe0000000000000ull, 0x3fee07f1d54c3f34ull}, {3, 0x3fe91c9d97f75a72ull, 0x3f83d672272043afull}, {3, 0x3ff33bf06c96fafdull, 0x3fa0ea842d47fdfdull}, + {3, 0x3ffa0a4f9419df40ull, 0x3fb36db8f4e44fabull}, {3, 0x3ffb2f9912a3a9a6ull, 0x3fb5c4cf9b0be24full}, + }; + CheckOrder("jn", jn_nid_postfix, first, std::size(first)); + const OrderCase second[] = { + {2, 0x7ff0000000000000ull, 0x0000000000000000ull}, {-3, 0x7ff0000000000000ull, 0x0000000000000000ull}, {3, 0x0000000000000000ull, 0xfff0000000000000ull}, + {-4, 0x8000000000000000ull, 0xfff0000000000000ull}, {3, 0xbff0000000000000ull, 0x7ff8000000000000ull}, {5, 0x7ff8000000000123ull, 0x7ff8000000000123ull}, + {INT_MIN, 0x7ff0000000000000ull, 0x0000000000000000ull}, + }; + CheckOrder("yn", yn_nid_postfix, second, std::size(second)); + for (const double x : {0.75, 2.5, 3.0, 17.25, 1.0e5, 0x1p303}) { + Require(Bits(jn_nid_postfix(0, x)) == Bits(j0_nid_postfix(x)) && Bits(jn_nid_postfix(1, x)) == Bits(j1_nid_postfix(x))); + Require(Bits(jn_nid_postfix(-1, x)) == Bits(j1_nid_postfix(-x)) && Bits(jn_nid_postfix(-3, x)) == Bits(-jn_nid_postfix(3, x))); + Require(Bits(jn_nid_postfix(4, -x)) == Bits(jn_nid_postfix(4, x)) && Bits(jn_nid_postfix(5, -x)) == Bits(-jn_nid_postfix(5, x))); + Require(Bits(yn_nid_postfix(0, x)) == Bits(y0_nid_postfix(x)) && Bits(yn_nid_postfix(1, x)) == Bits(y1_nid_postfix(x))); + Require(Bits(yn_nid_postfix(-1, x)) == Bits(-y1_nid_postfix(x)) && Bits(yn_nid_postfix(-4, x)) == Bits(yn_nid_postfix(4, x))); + if (x < 0x1p302) Require(Bits(yn_nid_postfix(INT_MIN, x)) == Bits(yn_nid_postfix(2, x))); + } +} + +static bool Near(double actual, double expected, double tolerance) { + return std::fabs(actual - expected) <= std::fabs(expected) * tolerance; +} + +struct ReferenceCase { int order; double x; double value; }; + +static void CheckValues() { + const ReferenceCase first[] = { + {0, 0.3, 0.97762624653829609}, {0, 1.7, 0.39798485944610949}, {0, 2.5, -0.048383776468197996}, {0, 7.25, 0.29199692419177900}, + {0, 25.0, 0.096266783275958116}, {0, 123.5, -0.070484621663252289}, {1, 0.3, 0.14831881627310401}, {1, 1.7, 0.57776523152902322}, + {1, 2.5, 0.49709410246427404}, {1, 7.25, 0.068581700653131745}, {1, 25.0, -0.12535024958028990}, {1, 123.5, -0.013948992295178035}, + {2, 0.3, 0.011165861949063964}, {2, 1.7, 0.28173894235274136}, {2, 2.5, 0.44605905843961723}, {2, 7.25, -0.27307783435643231}, + {2, 25.0, -0.10629480324238131}, {2, 123.5, 0.070258727051184628}, {3, 0.3, 0.00055934304774884612}, {3, 1.7, 0.085149926948015264}, + {3, 2.5, 0.21660039103911352}, {3, 7.25, -0.21924533340150819}, {3, 25.0, 0.10834308106150890}, {3, 123.5, 0.016224578596430978}, + {7, 0.3, 3.3805443102187481e-10}, {7, 1.7, 5.8087210824311728e-5}, {7, 2.5, 0.00077655318753348495}, {7, 7.25, 0.25934915542688296}, + {7, 25.0, -0.010168168212703074}, {7, 123.5, 0.027311804641119322}, + }; + const ReferenceCase second[] = { + {0, 0.3, -0.80727357780451947}, {0, 1.7, 0.45202700018163463}, {0, 2.5, 0.49807035961523189}, {0, 7.25, 0.048422529865202829}, + {0, 25.0, -0.12724943226800614}, {0, 123.5, -0.013663522162735081}, {1, 0.3, -2.2931051383885290}, {1, 1.7, -0.28472624506406838}, + {1, 2.5, 0.14591813796678580}, {1, 7.25, -0.28934799419758705}, {1, 25.0, -0.098829964783237410}, {1, 123.5, 0.070429882264699084}, + {2, 0.3, -14.480094011452341}, {2, 1.7, -0.78699905319818566}, {2, 2.5, -0.38133584924180325}, {2, 7.25, -0.12824266619557167}, + {2, 25.0, 0.11934303508534715}, {2, 123.5, 0.014804087057709964}, {3, 0.3, -190.77481501430935}, {3, 1.7, -1.5670362330493096}, + {3, 2.5, -0.75605549675367100}, {3, 7.25, 0.21859341974485785}, {3, 25.0, 0.11792485039689295}, {3, 123.5, -0.069950397663639652}, + {7, 0.3, -1.3463966616414596e+8}, {7, 1.7, -807.61347285004828}, {7, 2.5, -62.829855967240050}, {7, 7.25, -0.35448064613382143}, + {7, 25.0, 0.16252257251113247}, {7, 123.5, -0.066461555200665692}, + }; + for (const auto& test : first) { + const double result = test.order == 0 ? j0_nid_postfix(test.x) : test.order == 1 ? j1_nid_postfix(test.x) : jn_nid_postfix(test.order, test.x); + if (!Near(result, test.value, 1e-12) || !Near(jn_nid_postfix(test.order, -test.x), test.order % 2 != 0 ? -test.value : test.value, 1e-12)) { + std::fprintf(stderr, "Guest J%d(%g) = %.17g, expected %.17g\n", test.order, test.x, result, test.value); + std::abort(); + } + } + for (const auto& test : second) { + const double result = test.order == 0 ? y0_nid_postfix(test.x) : test.order == 1 ? y1_nid_postfix(test.x) : yn_nid_postfix(test.order, test.x); + if (!Near(result, test.value, 1e-12) || !Near(yn_nid_postfix(test.order, test.x), test.value, 1e-12)) { + std::fprintf(stderr, "Guest Y%d(%g) = %.17g, expected %.17g\n", test.order, test.x, result, test.value); + std::abort(); + } + } + *__error_nid_postfix() = 12345; + for (const double x : {0x1p-30, 0.5, 3.0, 1.0e10, 0x1p310, std::numeric_limits::max()}) { + Require(std::isfinite(j0_nid_postfix(x)) && std::isfinite(j1_nid_postfix(x)) && std::isfinite(y0_nid_postfix(x)) && std::isfinite(y1_nid_postfix(x))); + Require(std::isfinite(jn_nid_postfix(6, x)) && !std::isnan(yn_nid_postfix(6, x))); + } + Require(*__error_nid_postfix() == 12345); + *__error_nid_postfix() = 0; +} + +int main() { + CheckFirstKind(); + CheckSecondKind(); + CheckIntegerOrders(); + CheckValues(); + return 0; +} diff --git a/core/libs/tests/GuestBluetoothHid.cpp b/core/libs/tests/GuestBluetoothHid.cpp index f85b1d9ae1..67b25d22c6 100644 --- a/core/libs/tests/GuestBluetoothHid.cpp +++ b/core/libs/tests/GuestBluetoothHid.cpp @@ -12,6 +12,11 @@ std::int32_t APS5_VABI sceBluetoothHidUnregisterCallback(); std::int32_t APS5_VABI sceBluetoothHidRegisterDevice(std::uint16_t vendorId, std::uint16_t productId); std::int32_t APS5_VABI sceBluetoothHidUnregisterDevice(std::uint16_t vendorId, std::uint16_t productId); int APS5_VABI sceBluetoothHidGetInputReport(); +int APS5_VABI sceBluetoothHidDebugGetVersion(); +int APS5_VABI sceBluetoothHidGetDeviceInfo(); +int APS5_VABI sceBluetoothHidDisconnectDevice(); +int APS5_VABI sceBluetoothHidGetFeatureReport(); +int APS5_VABI sceBluetoothHidSetFeatureReport(); } namespace { @@ -51,6 +56,41 @@ int main() { threw = true; } Require(threw, "an input report without a device did not throw"); + threw = false; + try { + sceBluetoothHidDebugGetVersion(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "debug get version did not throw"); + threw = false; + try { + sceBluetoothHidGetDeviceInfo(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "get device info did not throw"); + threw = false; + try { + sceBluetoothHidDisconnectDevice(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "disconnect device did not throw"); + threw = false; + try { + sceBluetoothHidGetFeatureReport(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "get feature report did not throw"); + threw = false; + try { + sceBluetoothHidSetFeatureReport(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "set feature report did not throw"); Require(sceBluetoothHidUnregisterDevice(0x44f, 0x1000) == 0, "device unregistration failed"); Require(sceBluetoothHidUnregisterCallback() == 0, "callback unregistration failed"); std::puts("BluetoothHid tests passed"); diff --git a/core/libs/tests/GuestCesUnicode.cpp b/core/libs/tests/GuestCesUnicode.cpp new file mode 100644 index 0000000000..1603f09810 --- /dev/null +++ b/core/libs/tests/GuestCesUnicode.cpp @@ -0,0 +1,252 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceCesCs/JisX0208.hpp" +#include "prx/libSceCesCs/JisX0212.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceCesUcs2ToUtf8(uint16_t, uint8_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf8ToUcs2(const uint8_t*, uint32_t, uint32_t*, uint16_t*); +int APS5_VABI sceCesUtf32ToUtf8(uint32_t, uint8_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf32beToUtf8(const uint32_t*, uint8_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf32leToUtf8(const uint32_t*, uint8_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf8ToUtf32(const uint8_t*, uint32_t, uint32_t*, uint32_t*); +int APS5_VABI sceCesUtf8ToUtf32be(const uint8_t*, uint32_t, uint32_t*, uint32_t*); +int APS5_VABI sceCesUtf8ToUtf32le(const uint8_t*, uint32_t, uint32_t*, uint32_t*); +int APS5_VABI sceCesUtf8ToUtf16(const uint8_t*, uint32_t, uint32_t*, uint16_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf8ToUtf16be(const uint8_t*, uint32_t, uint32_t*, uint16_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf8ToUtf16le(const uint8_t*, uint32_t, uint32_t*, uint16_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf16ToUtf8(const uint16_t*, uint32_t, uint32_t*, uint8_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf16beToUtf8(const uint16_t*, uint32_t, uint32_t*, uint8_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf16leToUtf8(const uint16_t*, uint32_t, uint32_t*, uint8_t*, uint32_t, uint32_t*); +void* APS5_VABI sceCesUcsProfileInitEucJpCp51932(void*); +int APS5_VABI sceCesEucJpToUtf8(const void*, const uint8_t*, uint32_t, uint32_t*, uint8_t*, uint32_t, uint32_t*); +} + +namespace { + +constexpr int INVALID_PARAMETER = static_cast(0x805C0001); +constexpr int INVALID_SRC_BUFFER = static_cast(0x805C0010); +constexpr int SRC_BUFFER_END = static_cast(0x805C0011); +constexpr int INVALID_ENCODE = static_cast(0x805C0014); +constexpr int UNASSIGNED_CODE = static_cast(0x805C0020); +constexpr int INVALID_DST_BUFFER = static_cast(0x805C0030); +constexpr int DST_BUFFER_END = static_cast(0x805C0031); +constexpr uint32_t SENTINEL = 0xA5A5A5A5; + +void Require(bool value) { + if (!value) std::abort(); +} + +void Hash(uint64_t& hash, const void* data, uint32_t size) { + const auto* bytes = static_cast(data); + for (uint32_t i = 0; i < size; ++i) hash = (hash ^ bytes[i]) * 1099511628211ull; +} + +void TestByteOrder() { + const uint8_t source[] = {0xF0, 0x9F, 0x98, 0x80}; + const uint8_t expected16be[] = {0xD8, 0x3D, 0xDE, 0x00}; + const uint8_t expected16le[] = {0x3D, 0xD8, 0x00, 0xDE}; + const uint8_t expected32be[] = {0x00, 0x01, 0xF6, 0x00}; + const uint8_t expected32le[] = {0x00, 0xF6, 0x01, 0x00}; + uint16_t units[2] = {}; + uint32_t scalar = 0; + Require(sceCesUtf8ToUtf16be(source, 4, nullptr, units, 2, nullptr) == 0 && std::memcmp(units, expected16be, 4) == 0); + Require(sceCesUtf8ToUtf16le(source, 4, nullptr, units, 2, nullptr) == 0 && std::memcmp(units, expected16le, 4) == 0); + Require(sceCesUtf8ToUtf32be(source, 4, nullptr, &scalar) == 0 && std::memcmp(&scalar, expected32be, 4) == 0); + Require(sceCesUtf8ToUtf32le(source, 4, nullptr, &scalar) == 0 && std::memcmp(&scalar, expected32le, 4) == 0); + const uint8_t bom[] = {0xEF, 0xBB, 0xBF}; + Require(sceCesUtf8ToUtf32(bom, 3, nullptr, &scalar) == 0 && scalar == 0xFEFF); + Require(sceCesUtf8ToUtf16(bom, 3, nullptr, units, 1, nullptr) == 0 && units[0] == 0xFEFF); +} + +void TestScalars() { + uint64_t utf8Hash = 14695981039346656037ull; + uint64_t utf16Hash = utf8Hash; + for (uint32_t code = 0; code <= 0x10FFFF; ++code) { + if (code >= 0xD800 && code <= 0xDFFF) continue; + std::array bytes; + bytes.fill(0xA5); + uint32_t produced = SENTINEL; + Require(sceCesUtf32ToUtf8(code, bytes.data(), 4, &produced) == 0); + Require(produced >= 1 && produced <= 4 && bytes[produced] == 0xA5); + Hash(utf8Hash, bytes.data(), produced); + uint32_t decoded = SENTINEL; + uint32_t consumed = SENTINEL; + Require(sceCesUtf8ToUtf32(bytes.data(), produced, &consumed, &decoded) == 0); + Require(decoded == code && consumed == produced); + + uint16_t ucs2 = 0xA5A5; + consumed = SENTINEL; + const int ucsResult = sceCesUtf8ToUcs2(bytes.data(), produced, &consumed, &ucs2); + std::array back; + back.fill(0xA5); + uint32_t size = SENTINEL; + if (code <= 0xFFFF) { + Require(ucsResult == 0 && ucs2 == code && consumed == produced); + Require(sceCesUcs2ToUtf8(ucs2, back.data(), 4, &size) == 0); + Require(size == produced && std::memcmp(back.data(), bytes.data(), size) == 0); + } else Require(ucsResult == UNASSIGNED_CODE && ucs2 == 0xA5A5 && consumed == SENTINEL); + + using To16 = int (APS5_VABI *)(const uint8_t*, uint32_t, uint32_t*, uint16_t*, uint32_t, uint32_t*); + using From16 = int (APS5_VABI *)(const uint16_t*, uint32_t, uint32_t*, uint8_t*, uint32_t, uint32_t*); + const To16 to16[] = {sceCesUtf8ToUtf16, sceCesUtf8ToUtf16be, sceCesUtf8ToUtf16le}; + const From16 from16[] = {sceCesUtf16ToUtf8, sceCesUtf16beToUtf8, sceCesUtf16leToUtf8}; + for (uint32_t variant = 0; variant < 3; ++variant) { + uint16_t units[3] = {0xA5A5, 0xA5A5, 0xA5A5}; + uint32_t unitCount = SENTINEL; + consumed = SENTINEL; + Require(to16[variant](bytes.data(), produced, &consumed, units, 2, &unitCount) == 0); + Require(consumed == produced && unitCount == (code <= 0xFFFF ? 1u : 2u) && units[unitCount] == 0xA5A5); + if (variant == 2) Hash(utf16Hash, units, 2 * unitCount); + consumed = size = SENTINEL; + Require(from16[variant](units, unitCount, &consumed, back.data(), 4, &size) == 0); + Require(consumed == unitCount && size == produced && std::memcmp(back.data(), bytes.data(), size) == 0); + } + + using To32 = int (APS5_VABI *)(const uint8_t*, uint32_t, uint32_t*, uint32_t*); + using From32 = int (APS5_VABI *)(const uint32_t*, uint8_t*, uint32_t, uint32_t*); + const To32 to32[] = {sceCesUtf8ToUtf32be, sceCesUtf8ToUtf32le}; + const From32 from32[] = {sceCesUtf32beToUtf8, sceCesUtf32leToUtf8}; + for (uint32_t variant = 0; variant < 2; ++variant) { + uint32_t stored[2] = {SENTINEL, SENTINEL}; + Require(to32[variant](bytes.data(), produced, &consumed, stored) == 0); + Require(consumed == produced && stored[1] == SENTINEL); + const auto* raw = reinterpret_cast(stored); + Require(raw[variant == 0 ? 0 : 3] == 0 && raw[variant == 0 ? 3 : 0] == (code & 0xFF)); + Require(from32[variant](stored, back.data(), 4, &size) == 0); + Require(size == produced && std::memcmp(back.data(), bytes.data(), size) == 0); + } + } + Require(utf8Hash == 0x957CC0987E6013E5ull); + Require(utf16Hash == 0x6A25A430597EC325ull); + std::puts("1112064 Unicode scalars match Python UTF-8 and UTF-16LE reference hashes"); +} + +void TestErrors() { + const uint8_t emoji[] = {0xF0, 0x9F, 0x98, 0x80, 'X'}; + uint32_t consumed = SENTINEL; + uint32_t produced = SENTINEL; + uint32_t scalar = SENTINEL; + uint16_t units[3] = {0xA5A5, 0xA5A5, 0xA5A5}; + uint8_t bytes[5] = {0xA5, 0xA5, 0xA5, 0xA5, 0xA5}; + for (uint32_t cap = 0; cap < 4; ++cap) { + Require(sceCesUtf32ToUtf8(0x1F600, bytes, cap, &produced) == DST_BUFFER_END); + Require(produced == SENTINEL && bytes[0] == 0xA5 && bytes[3] == 0xA5); + Require(sceCesUtf8ToUtf32(emoji, cap, &consumed, &scalar) == SRC_BUFFER_END); + Require(consumed == SENTINEL && scalar == SENTINEL); + } + for (uint32_t cap = 0; cap < 2; ++cap) { + Require(sceCesUtf8ToUtf16(emoji, 4, &consumed, units, cap, &produced) == DST_BUFFER_END); + Require(consumed == SENTINEL && produced == SENTINEL && units[0] == 0xA5A5 && units[1] == 0xA5A5); + } + const uint8_t invalid[][4] = {{0x80}, {0xC0, 0x80}, {0xC1, 0xBF}, {0xC2, 0x7F}, {0xE0, 0x9F}, + {0xED, 0xA0}, {0xF0, 0x8F}, {0xF4, 0x90}, {0xF5}, {0xFF}, {0xE2, 0x82, 0x41}, {0xF0, 0x9F, 0x98, 0x41}}; + const uint32_t lengths[] = {1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 3, 4}; + for (uint32_t i = 0; i < std::size(invalid); ++i) { + Require(sceCesUtf8ToUtf32(invalid[i], lengths[i], &consumed, &scalar) == INVALID_ENCODE); + Require(sceCesUtf8ToUtf16be(invalid[i], lengths[i], &consumed, units, 2, &produced) == INVALID_ENCODE); + Require(consumed == SENTINEL && produced == SENTINEL && scalar == SENTINEL && units[0] == 0xA5A5); + } + for (uint32_t code : {0xD800u, 0xDBFFu, 0xDC00u, 0xDFFFu, 0x110000u, 0xFFFFFFFFu}) { + Require(sceCesUtf32ToUtf8(code, bytes, 4, &produced) == INVALID_ENCODE); + if (code <= 0xFFFF) Require(sceCesUcs2ToUtf8(static_cast(code), bytes, 4, &produced) == INVALID_ENCODE); + Require(produced == SENTINEL && bytes[0] == 0xA5); + } + const uint16_t high[] = {0xD800, 0x0041}; + const uint16_t low[] = {0xDC00}; + Require(sceCesUtf16ToUtf8(high, 1, &consumed, bytes, 4, &produced) == SRC_BUFFER_END); + Require(sceCesUtf16ToUtf8(high, 2, &consumed, bytes, 4, &produced) == INVALID_ENCODE); + Require(sceCesUtf16ToUtf8(low, 1, &consumed, bytes, 4, &produced) == INVALID_ENCODE); + Require(consumed == SENTINEL && produced == SENTINEL && bytes[0] == 0xA5); + Require(sceCesUtf8ToUtf32(nullptr, 4, &consumed, &scalar) == INVALID_SRC_BUFFER); + Require(sceCesUtf8ToUtf32(emoji, 4, &consumed, nullptr) == INVALID_DST_BUFFER); + Require(sceCesUtf8ToUcs2(emoji, 4, &consumed, nullptr) == INVALID_DST_BUFFER); + Require(sceCesUtf8ToUtf16(emoji, 4, &consumed, nullptr, 2, &produced) == INVALID_DST_BUFFER); + Require(sceCesUtf16ToUtf8(nullptr, 1, &consumed, bytes, 4, &produced) == INVALID_SRC_BUFFER); + Require(sceCesUtf16ToUtf8(high, 0, &consumed, bytes, 4, &produced) == SRC_BUFFER_END); + Require(sceCesUtf32beToUtf8(nullptr, bytes, 4, &produced) == INVALID_SRC_BUFFER); + Require(sceCesUtf32ToUtf8(0, nullptr, 4, &produced) == INVALID_DST_BUFFER); + Require(sceCesUtf8ToUtf32(emoji, 5, nullptr, &scalar) == 0 && scalar == 0x1F600); + Require(sceCesUtf8ToUtf16(emoji, 5, nullptr, units, 2, nullptr) == 0 && units[0] == 0xD83D && units[1] == 0xDE00); + Require(sceCesUtf16ToUtf8(units, 2, nullptr, bytes, 4, nullptr) == 0 && std::memcmp(bytes, emoji, 4) == 0); + Require(sceCesUcs2ToUtf8(0, bytes, 1, nullptr) == 0 && bytes[0] == 0); + Require(sceCesUtf8ToUcs2(bytes, 1, nullptr, units) == 0 && units[0] == 0); +} + +void TestEucJp() { + std::array sheet{}; + const void* profile = sceCesUcsProfileInitEucJpCp51932(sheet.data()); + auto check = [&](const uint8_t* source, uint32_t length, uint32_t code) { + uint8_t expected[4] = {}; + uint32_t expectedLength = 0; + Require(sceCesUtf32ToUtf8(code, expected, 4, &expectedLength) == 0); + std::array output; + output.fill(0xA5); + uint32_t consumed = SENTINEL; + uint32_t produced = SENTINEL; + Require(sceCesEucJpToUtf8(profile, source, length, &consumed, output.data(), expectedLength, &produced) == 0); + Require(consumed == length && produced == expectedLength && output[produced] == 0xA5); + Require(std::memcmp(output.data(), expected, produced) == 0); + for (uint32_t cap = 0; cap < expectedLength; ++cap) { + output.fill(0xA5); + consumed = produced = SENTINEL; + Require(sceCesEucJpToUtf8(profile, source, length, &consumed, output.data(), cap, &produced) == DST_BUFFER_END); + Require(consumed == SENTINEL && produced == SENTINEL && output[0] == 0xA5); + } + }; + for (uint32_t code = 0; code < 128; ++code) { + const uint8_t source[] = {static_cast(code)}; + check(source, 1, code); + } + for (uint32_t cell = 0; cell < 63; ++cell) { + const uint8_t source[] = {0x8E, static_cast(0xA1 + cell)}; + check(source, 2, 0xFF61 + cell); + } + uint32_t assigned = 0; + for (uint32_t supplementary = 0; supplementary < 2; ++supplementary) { + for (uint32_t cell = 0; cell < 94 * 94; ++cell) { + const uint32_t code = supplementary ? JisX0212::TO_UCS2[cell] : JisX0208::TO_UCS2[cell]; + const uint8_t source[] = {0x8F, static_cast(0xA1 + cell / 94), static_cast(0xA1 + cell % 94)}; + const uint8_t* start = source + (supplementary ? 0 : 1); + const uint32_t length = supplementary ? 3 : 2; + if (code) { check(start, length, code); ++assigned; } + else { + uint8_t output[4] = {0xA5, 0xA5, 0xA5, 0xA5}; + uint32_t consumed = SENTINEL; + uint32_t produced = SENTINEL; + Require(sceCesEucJpToUtf8(profile, start, length, &consumed, output, 4, &produced) == UNASSIGNED_CODE); + Require(consumed == SENTINEL && produced == SENTINEL && output[0] == 0xA5); + } + } + } + Require(assigned == 13403); + const uint8_t source[] = {0x8F, 0xA2, 0xAF, 'X'}; + uint8_t output[4] = {0xA5, 0xA5, 0xA5, 0xA5}; + uint32_t consumed = SENTINEL; + uint32_t produced = SENTINEL; + Require(sceCesEucJpToUtf8(nullptr, source, 3, &consumed, output, 4, &produced) == INVALID_PARAMETER); + Require(sceCesEucJpToUtf8(profile, nullptr, 3, &consumed, output, 4, &produced) == INVALID_SRC_BUFFER); + Require(sceCesEucJpToUtf8(profile, source, 0, &consumed, output, 4, &produced) == SRC_BUFFER_END); + Require(sceCesEucJpToUtf8(profile, source, 3, &consumed, nullptr, 4, &produced) == INVALID_DST_BUFFER); + for (uint32_t size : {1u, 2u}) Require(sceCesEucJpToUtf8(profile, source, size, &consumed, output, 4, &produced) == SRC_BUFFER_END); + const uint8_t malformed[] = {0x8E, 0x41}; + Require(sceCesEucJpToUtf8(profile, malformed, 2, &consumed, output, 4, &produced) == INVALID_ENCODE); + Require(consumed == SENTINEL && produced == SENTINEL && output[0] == 0xA5); + Require(sceCesEucJpToUtf8(profile, source, 4, &consumed, output, 4, &produced) == 0); + Require(consumed == 3 && produced == 2 && output[0] == 0xCB && output[1] == 0x98 && output[2] == 0xA5); + Require(sceCesEucJpToUtf8(profile, source, 4, nullptr, output, 4, nullptr) == 0); + std::puts("13403 assigned JIS cells plus ASCII, half-width katakana and unassigned cells checked"); +} + +} + +int main() { + TestByteOrder(); + TestErrors(); + TestScalars(); + TestEucJp(); +} diff --git a/core/libs/tests/GuestContentExport.cpp b/core/libs/tests/GuestContentExport.cpp index ef295d6ec9..40c6aa941d 100644 --- a/core/libs/tests/GuestContentExport.cpp +++ b/core/libs/tests/GuestContentExport.cpp @@ -4,6 +4,19 @@ #include #include +extern "C" int APS5_VABI sceContentExportAddToFavorites(void); +extern "C" int APS5_VABI sceContentExportDataProvideFunction(void); +extern "C" int APS5_VABI sceContentExportFree(void); +extern "C" int APS5_VABI sceContentExportFrom(void); +extern "C" int APS5_VABI sceContentExportFromFileForSf(void); +extern "C" int APS5_VABI sceContentExportFromFileWithContentIdList(void); +extern "C" int APS5_VABI sceContentExportFromFileWithTitleIdList(void); +extern "C" int APS5_VABI sceContentExportInit(void); +extern "C" int APS5_VABI sceContentExportMalloc(void); +extern "C" int APS5_VABI sceContentExportRemoveFromFavorites(void); +extern "C" int APS5_VABI sceContentExportRenameContent(void); +extern "C" int APS5_VABI sceContentExportValidateContents(void); + extern "C" int APS5_VABI sceContentExportInit2(const ContentExportInitParam2* init_param); extern "C" int APS5_VABI sceContentExportTerm(void); extern "C" int APS5_VABI sceContentExportStart(void); @@ -85,4 +98,17 @@ int main() { Require(sceContentExportTerm() == 0); Require(Throws([] { sceContentExportStart(); })); Require(Throws([&] { sceContentExportFinish(reopened); })); + Require(Throws([] { sceContentExportAddToFavorites(); })); + Require(Throws([] { sceContentExportDataProvideFunction(); })); + Require(Throws([] { sceContentExportFree(); })); + Require(Throws([] { sceContentExportFrom(); })); + Require(Throws([] { sceContentExportFromFileForSf(); })); + Require(Throws([] { sceContentExportFromFileWithContentIdList(); })); + Require(Throws([] { sceContentExportFromFileWithTitleIdList(); })); + Require(Throws([] { sceContentExportInit(); })); + Require(Throws([] { sceContentExportMalloc(); })); + Require(Throws([] { sceContentExportRemoveFromFavorites(); })); + Require(Throws([] { sceContentExportRenameContent(); })); + Require(Throws([] { sceContentExportValidateContents(); })); } + diff --git a/core/libs/tests/GuestCredentials.cpp b/core/libs/tests/GuestCredentials.cpp index a87d68f39e..887f69c727 100644 --- a/core/libs/tests/GuestCredentials.cpp +++ b/core/libs/tests/GuestCredentials.cpp @@ -8,6 +8,7 @@ extern "C" { int APS5_VABI seteuid_nid_postfix(std::uint32_t); int APS5_VABI setegid_nid_postfix(std::uint32_t); int APS5_VABI setuid_nid_postfix(std::uint32_t); +int APS5_VABI setgid_nid_postfix(std::uint32_t); int APS5_VABI setreuid_nid_postfix(std::uint32_t, std::uint32_t); int APS5_VABI setregid_nid_postfix(std::uint32_t, std::uint32_t); int APS5_VABI setgroups_nid_postfix(int, const std::uint32_t*); @@ -62,6 +63,9 @@ int main() { Require(Throws(setegid_nid_postfix, Unchanged)); Require(setuid_nid_postfix(0) == 0); + Require(setgid_nid_postfix(0) == 0); + Require(Throws(setgid_nid_postfix, 1000)); + Require(Throws(setgid_nid_postfix, Unchanged)); Require(getuid_nid_postfix() == 0 && geteuid_nid_postfix() == 0); Require(getgid_nid_postfix() == 0 && getegid_nid_postfix() == 0 && issetugid_nid_postfix() == 0); Require(setreuid_nid_postfix(Unchanged, Unchanged) == 0); diff --git a/core/libs/tests/GuestCrt.cpp b/core/libs/tests/GuestCrt.cpp index df087488e3..d2fe45d744 100644 --- a/core/libs/tests/GuestCrt.cpp +++ b/core/libs/tests/GuestCrt.cpp @@ -189,10 +189,20 @@ int main() { Require(asctime_s_nid_postfix(text, sizeof(text), &known) == 0); Require(std::strcmp(text, "Thu Jan 1 00:00:00 1970\n") == 0); known.tm_wday = 7; - Require(sceAmprAsctimeS(text, sizeof(text), &known) == EINVAL); + text[0] = 'x'; + Require(sceAmprAsctimeS(text, sizeof(text), &known) == EINVAL && text[0] == '\0'); known.tm_wday = 4; + known.tm_year = 9000; + text[0] = 'x'; + Require(sceAmprAsctimeS(text, sizeof(text), &known) == EINVAL && text[0] == '\0'); + known.tm_year = 70; + known.tm_mday = 32; + text[0] = 'x'; + Require(sceAmprAsctimeS(text, sizeof(text), &known) == EINVAL && text[0] == '\0'); + known.tm_mday = 1; Require(sceAmprAsctimeS(nullptr, sizeof(text), &known) == EINVAL); - Require(sceAmprAsctimeS(text, sizeof(text), nullptr) == EINVAL); + text[0] = 'x'; + Require(sceAmprAsctimeS(text, sizeof(text), nullptr) == EINVAL && text[0] == '\0'); Require(sceAmprAsctimeS(text, 25, &known) == EINVAL); const std::int64_t epoch = 0; Require(sceAmprCtimeS(text, sizeof(text), &epoch) == 0); @@ -201,7 +211,8 @@ int main() { Require(ctime_s_nid_postfix(libcText, sizeof(libcText), &epoch) == 0); Require(std::strcmp(libcText, text) == 0); Require(sceAmprCtimeS(text, 25, &epoch) == EINVAL); - Require(sceAmprCtimeS(text, sizeof(text), nullptr) == EINVAL); + text[0] = 'x'; + Require(sceAmprCtimeS(text, sizeof(text), nullptr) == EINVAL && text[0] == '\0'); char line[] = "a,b,,c"; char* context = nullptr; diff --git a/core/libs/tests/GuestFdopendir.cpp b/core/libs/tests/GuestFdopendir.cpp index 4e6cfffa82..551ad063f0 100644 --- a/core/libs/tests/GuestFdopendir.cpp +++ b/core/libs/tests/GuestFdopendir.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/File/include/File.hpp" #include +#include #include #include #include @@ -14,6 +15,8 @@ GuestDirectoryEntry* APS5_VABI readdir_nid_postfix(void*); int APS5_VABI closedir_nid_postfix(void*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); int* APS5_VABI __error_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } @@ -25,7 +28,9 @@ int main() { { std::ofstream file(root / "sample.txt"); file << "test"; } const int fd = open_nid_postfix(root.string().c_str(), 0x20000, 0); Require(fd >= 0); + Require(fcntl_nid_postfix(fd, 1) == 0); void* directory = fdopendir_nid_postfix(fd); + Require(fcntl_nid_postfix(fd, 1) == 1); Require(directory != nullptr); std::map entries; while (auto* entry = readdir_nid_postfix(directory)) { @@ -41,8 +46,20 @@ int main() { const int file = open_nid_postfix((root / "sample.txt").string().c_str(), 0, 0); Require(file >= 0); Require(fdopendir_nid_postfix(file) == nullptr && *__error_nid_postfix() == 20); + Require(fcntl_nid_postfix(file, 1) == 1); Require(DescriptorIsOpen_nid_no_patch(file)); Require(close_nid_postfix(file) == 0); +#ifdef _WIN32 + const int advanced = open_nid_postfix(root.string().c_str(), 0x20000, 0); + Require(advanced >= 0); + Require(lseek_nid_postfix(advanced, 2, 0) == 2); + void* rest = fdopendir_nid_postfix(advanced); + Require(rest != nullptr); + std::map remaining; + while (auto* entry = readdir_nid_postfix(rest)) remaining[entry->name] = entry->type; + Require(remaining.size() == 2 && remaining.count("sample.txt") == 1 && remaining.count("subdirectory") == 1); + Require(closedir_nid_postfix(rest) == 0); +#endif std::filesystem::remove(root / "sample.txt"); std::filesystem::remove(root / "subdirectory"); std::filesystem::remove(root); diff --git a/core/libs/tests/GuestFileLock.cpp b/core/libs/tests/GuestFileLock.cpp index 0a49b8d102..3fb7ac7b71 100644 --- a/core/libs/tests/GuestFileLock.cpp +++ b/core/libs/tests/GuestFileLock.cpp @@ -35,10 +35,10 @@ static void Require(bool condition) { if (!condition) std::abort(); } -static int Duplicate(int source, int kind) { +static int Duplicate(int source, int kind, int target = 20) { if (kind == 0) return dup_nid_postfix(source); - if (kind == 1) return dup2_nid_postfix(source, 20); - return fcntl_nid_postfix(source, 0, 20); + if (kind == 1) return dup2_nid_postfix(source, target); + return fcntl_nid_postfix(source, 0, target); } static bool Acquires(int descriptor, int mode) { @@ -64,8 +64,8 @@ struct Fixture { descriptors.insert(descriptor); return descriptor; } - int Copy(int kind, int from = -1) { - const int descriptor = Duplicate(from < 0 ? source : from, kind); + int Copy(int kind, int from = -1, int target = 20) { + const int descriptor = Duplicate(from < 0 ? source : from, kind, target); Require(descriptor >= 0); descriptors.insert(descriptor); return descriptor; @@ -258,10 +258,11 @@ static void Streams() { Require(fclose_nid_postfix(stream) == 0); } -static int ChildTry(const std::string& path, int mode) { +static int ChildTry(const Fixture (&fixtures)[6], int mode) { wchar_t executable[32768]; Require(::GetModuleFileNameW(nullptr, executable, 32768) != 0); - std::wstring command = L"\"" + std::wstring(executable) + L"\" child \"" + std::filesystem::path(path).wstring() + L"\" " + std::to_wstring(mode | 4); + std::wstring command = L"\"" + std::wstring(executable) + L"\" child " + std::to_wstring(mode | 4); + for (const Fixture& fixture : fixtures) command += L" \"" + std::filesystem::path(fixture.path).wstring() + L"\""; STARTUPINFOW startup{}; startup.cb = sizeof(startup); PROCESS_INFORMATION process{}; @@ -278,30 +279,38 @@ static int ChildTry(const std::string& path, int mode) { } static void Processes() { - for (int kind = 0; kind != 3; ++kind) { - for (int mode : {1, 2}) { - Fixture fixture; - Require(flock_nid_postfix(fixture.source, mode | 4) == 0); - const int copy = fixture.Copy(kind); - Require(ChildTry(fixture.path, 2) == 35); - Require(ChildTry(fixture.path, 1) == (mode == 1 ? 0 : 35)); - fixture.Close(fixture.source); - Require(ChildTry(fixture.path, 2) == 35); - Require(flock_nid_postfix(copy, 8) == 0); - Require(ChildTry(fixture.path, 2) == 0); - } + Fixture fixtures[6]; + int copies[6]; + int shared = 0; + for (int index = 0; index != 6; ++index) { + const int mode = index % 2 == 0 ? 1 : 2; + if (mode == 1) shared |= 1 << index; + Require(flock_nid_postfix(fixtures[index].source, mode | 4) == 0); + copies[index] = fixtures[index].Copy(index / 2, -1, 100 + index); } + Require(ChildTry(fixtures, 2) == 0x3f); + Require(ChildTry(fixtures, 1) == (0x3f & ~shared)); + for (Fixture& fixture : fixtures) fixture.Close(fixture.source); + Require(ChildTry(fixtures, 2) == 0x3f); + for (const int copy : copies) Require(flock_nid_postfix(copy, 8) == 0); + Require(ChildTry(fixtures, 2) == 0); } int main(int argc, char** argv) { - if (argc == 4 && std::string(argv[1]) == "child") { - const int descriptor = open_nid_postfix(argv[2], 2, 0); - Require(descriptor >= 0); - const int result = flock_nid_postfix(descriptor, std::atoi(argv[3])); - const int error = result == 0 ? 0 : *__error_nid_postfix(); - if (result == 0) Require(flock_nid_postfix(descriptor, 8) == 0); - Require(close_nid_postfix(descriptor) == 0); - return error; + if (argc >= 4 && std::string(argv[1]) == "child") { + int busy = 0; + for (int index = 3; index < argc; ++index) { + const int descriptor = open_nid_postfix(argv[index], 2, 0); + Require(descriptor >= 0); + if (flock_nid_postfix(descriptor, std::atoi(argv[2])) == 0) { + Require(flock_nid_postfix(descriptor, 8) == 0); + } else { + Require(*__error_nid_postfix() == 35); + busy |= 1 << (index - 3); + } + Require(close_nid_postfix(descriptor) == 0); + } + return busy; } Require(argc == 2); const std::string mode = argv[1]; diff --git a/core/libs/tests/GuestFileTimestamps.cpp b/core/libs/tests/GuestFileTimestamps.cpp index b1e91115f8..62750ec5f3 100644 --- a/core/libs/tests/GuestFileTimestamps.cpp +++ b/core/libs/tests/GuestFileTimestamps.cpp @@ -91,6 +91,15 @@ int main() { } } + const KernelTimeval invalidBeforePath[2]{{1000000000, -1}, {1000000001, 0}}; + for (const char* pathless : {static_cast(nullptr), ""}) { + Require(utimes_nid_postfix(pathless, invalidBeforePath) == -1 && *__error_nid_postfix() == 22, "utimes checks microseconds before the path"); + Require(lutimes_nid_postfix(pathless, invalidBeforePath) == -1 && *__error_nid_postfix() == 22, "lutimes checks microseconds before the path"); + Require(sceKernelUtimes_nid_postfix(pathless, invalidBeforePath) == static_cast(0x80020016u), "sceKernelUtimes checks microseconds before the path"); + } + Require(utimes_nid_postfix(nullptr, nullptr) == -1 && *__error_nid_postfix() == 14, "utimes still reports EFAULT for a null path"); + Require(utimes_nid_postfix("", nullptr) == -1 && *__error_nid_postfix() == 2, "utimes still reports ENOENT for an empty path"); + #ifdef _WIN32 const auto handle = CreateFileW(std::filesystem::path(path).c_str(), FILE_WRITE_ATTRIBUTES | FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, 0, nullptr); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 1921031174..7818a4421c 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -35,6 +35,7 @@ int APS5_VABI sceKernelFchmod(int, unsigned short); int APS5_VABI fchmod_nid_postfix(int, int); int APS5_VABI chmod_nid_postfix(const char*, int); int APS5_VABI chflags_nid_postfix(const char*, std::uint64_t); +int APS5_VABI fchflags_nid_postfix(int, std::uint64_t); int APS5_VABI futimes_nid_postfix(int, const KernelTimeval*); int APS5_VABI sceKernelFutimes(int, const KernelTimeval*); int APS5_VABI socket_nid_postfix(int, int, int); @@ -45,6 +46,7 @@ int APS5_VABI sceKernelFdatasync(int); int APS5_VABI sceKernelWriteThrottlingStatus(std::uint64_t*); int APS5_VABI sceKernelFtruncate(int, long long); int APS5_VABI sceKernelTruncate_nid_postfix(const char*, long long); +int APS5_VABI truncate_nid_postfix(const char*, long long); int APS5_VABI sceKernelUtimes_nid_postfix(const char*, const void*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI creat_nid_postfix(const char*, int); @@ -131,6 +133,15 @@ static bool ChflagsThrows(const std::filesystem::path& path, std::uint64_t flags return false; } +static bool FchflagsThrows(int descriptor, std::uint64_t flags, const char* names) { + try { + fchflags_nid_postfix(descriptor, flags); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), names) != nullptr; + } + return false; +} + static std::string CaptureStderr(const std::function& action) { std::FILE* capture = std::tmpfile(); Require(capture != nullptr); @@ -543,6 +554,9 @@ int main() { std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(mkdir_nid_postfix(root.string().c_str(), 0700) == 0); Require(mkdir_nid_postfix(root.string().c_str(), 0700) == -1 && *__error_nid_postfix() == 17); + for (const char* openMode : {"w", "wb", "r+", "a"}) { + Require(fopen_nid_postfix(root.string().c_str(), openMode) == nullptr && *__error_nid_postfix() == 21); + } Require(mkdir_nid_postfix((root / "missing" / "child").string().c_str(), 0700) == -1 && *__error_nid_postfix() == 2); Require(mkdir_nid_postfix(nullptr, 0700) == -1 && *__error_nid_postfix() == 14); const auto file = root / "file.txt"; @@ -561,6 +575,28 @@ 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 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); + Require(fchflags_nid_postfix(flagged, 1ULL << 32) == -1 && *__error_nid_postfix() == 45); + Require(fchflags_nid_postfix(flagged, ~0ULL) == -1 && *__error_nid_postfix() == 45); + Require(fchflags_nid_postfix(flagged, 0x200000) == -1 && *__error_nid_postfix() == 1); + Require(FchflagsThrows(flagged, 0x2, "UF_IMMUTABLE")); + Require(FchflagsThrows(flagged, 0x1000 | 0x40000, "UF_READONLY|SF_APPEND")); + Require(close_nid_postfix(flagged) == 0); + Require(fchflags_nid_postfix(flagged, 0) == -1 && *__error_nid_postfix() == 9); + Require(fchflags_nid_postfix(flagged, ~0ULL) == -1 && *__error_nid_postfix() == 9); + Require(fchflags_nid_postfix(-1, 0) == -1 && *__error_nid_postfix() == 9); + const int flaggedRoot = sceKernelOpen(root.string().c_str(), 0x20000, 0); + Require(flaggedRoot >= 0 && fchflags_nid_postfix(flaggedRoot, 0) == 0 && sceKernelClose(flaggedRoot) == 0); + int flaggedPipe[2]; + Require(pipe_nid_postfix(flaggedPipe) == 0); + Require(fchflags_nid_postfix(flaggedPipe[0], 0) == -1 && *__error_nid_postfix() == 22); + Require(fchflags_nid_postfix(flaggedPipe[1], ~0ULL) == -1 && *__error_nid_postfix() == 22); + Require(close_nid_postfix(flaggedPipe[0]) == 0 && close_nid_postfix(flaggedPipe[1]) == 0); + const int flaggedSocket = socket_nid_postfix(2, 2, 0); + Require(flaggedSocket >= 0 && fchflags_nid_postfix(flaggedSocket, 0) == -1 && *__error_nid_postfix() == 22); + Require(close_nid_postfix(flaggedSocket) == 0); const auto held = root / "held.txt"; const auto heldName = held.string(); { std::ofstream stream(held); stream << "held"; } @@ -732,7 +768,7 @@ int main() { Require(std::filesystem::file_size(sized) == 6); { std::ifstream stream(sized); std::string contents; std::getline(stream, contents); Require(contents == "012345"); } - Require(sceKernelTruncate_nid_postfix((sized / "missing").string().c_str(), 6) == static_cast(0x80020002u)); + Require(sceKernelTruncate_nid_postfix((sized / "missing").string().c_str(), 6) == static_cast(0x80020014u)); Require(sceKernelUtimes_nid_postfix(sized.string().c_str(), nullptr) == 0); std::FILE* native = std::fopen(sized.string().c_str(), "r+b"); Require(native != nullptr); @@ -780,6 +816,14 @@ int main() { Require(sceKernelFdatasync(socket) == static_cast(0x80020016u)); Require(fsync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 22); Require(sceKernelFsync(socket) == static_cast(0x80020016u)); + int syncPipe[2] = {-1, -1}; + Require(pipe_nid_postfix(syncPipe) == 0); + for (const int end : syncPipe) { + Require(fsync_nid_postfix(end) == -1 && *__error_nid_postfix() == 22); + Require(fdatasync_nid_postfix(end) == -1 && *__error_nid_postfix() == 22); + Require(sceKernelFsync(end) == static_cast(0x80020016u) && sceKernelFdatasync(end) == static_cast(0x80020016u)); + } + Require(close_nid_postfix(syncPipe[0]) == 0 && close_nid_postfix(syncPipe[1]) == 0); Require(close_nid_postfix(socket) == 0); Require(fchmod_nid_postfix(socket, 0600) == -1 && *__error_nid_postfix() == 9); Require(futimes_nid_postfix(socket, nullptr) == -1 && *__error_nid_postfix() == 9); @@ -817,6 +861,11 @@ int main() { const auto cwdPermissions = std::filesystem::status(".").permissions(); Require(sceKernelChmod_nid_postfix("", static_cast(cwdPermissions) & 07777) == static_cast(0x80020002u)); Require(sceKernelTruncate_nid_postfix("", 0) == static_cast(0x80020002u)); + Require(sceKernelTruncate_nid_postfix(nullptr, -1) == static_cast(0x80020016u)); + Require(sceKernelTruncate_nid_postfix("", -1) == static_cast(0x80020016u)); + Require(truncate_nid_postfix(nullptr, -1) == -1 && *__error_nid_postfix() == 22); + Require(truncate_nid_postfix("", -1) == -1 && *__error_nid_postfix() == 22); + Require(sceKernelTruncate_nid_postfix(nullptr, 0) == static_cast(0x8002000Eu)); const auto cwdWriteTime = std::filesystem::last_write_time("."); const int emptyUtimes = sceKernelUtimes_nid_postfix("", nullptr); std::error_code restoreTimeError; @@ -929,6 +978,11 @@ int main() { Require((junctionStatus.st_mode & 0170000) == 0120000 && junctionStatus.st_size > 0); FileStat junctionFollowed{}; Require(stat_nid_postfix(junction.string().c_str(), &junctionFollowed) == 0 && (junctionFollowed.st_mode & 0170000) == 0040000); + constexpr int noFollow = 0x100; + Require(sceKernelOpen(junction.string().c_str(), noFollow, 0) == static_cast(0x8002001Fu)); + Require(open_nid_postfix(junction.string().c_str(), noFollow, 0) == -1 && *__error_nid_postfix() == 31); + const int followedJunction = sceKernelOpen(junction.string().c_str(), 0, 0); + Require(followedJunction >= 0 && sceKernelClose(followedJunction) == 0); Require(std::filesystem::remove(junction) && std::filesystem::remove(junctionTarget)); #endif FileStat identity{}; @@ -972,6 +1026,13 @@ int main() { Require(flock_nid_postfix(other, 2 | 4) == 0); Require(flock_nid_postfix(other, 8) == 0); Require(flock_nid_postfix(locked, 2 | 4) == 0); + Require(flock_nid_postfix(-1, 2 | 4) == -1 && *__error_nid_postfix() == 9); + Require(flock_nid_postfix(0x7fffffff, 1) == -1 && *__error_nid_postfix() == 9); + int flockPipe[2] = {-1, -1}; + Require(pipe_nid_postfix(flockPipe) == 0); + Require(flock_nid_postfix(flockPipe[0], 2 | 4) == -1 && *__error_nid_postfix() == 45); + Require(flock_nid_postfix(flockPipe[1], 8) == -1 && *__error_nid_postfix() == 45); + Require(close_nid_postfix(flockPipe[0]) == 0 && close_nid_postfix(flockPipe[1]) == 0); Require(close_nid_postfix(locked) == 0); const int recycled = open_nid_postfix(presentName.c_str(), 0, 0); Require(recycled >= 0 && flock_nid_postfix(recycled, 8) == 0); @@ -1121,6 +1182,28 @@ int main() { Require(rmdir_nid_postfix((missingName + "/").c_str()) == -1 && *__error_nid_postfix() == 2); Require(sceKernelRmdir((presentName + "/").c_str()) == static_cast(0x80020014u)); Require(sceKernelRmdir((missingName + "/").c_str()) == static_cast(0x80020002u)); + const auto missingChild = missingName + "/child"; + for (const auto* blocked : {&underFile, &deepUnderFile, &missingChild}) { + const int expected = blocked == &missingChild ? 2 : 20; + FileStat blockedStatus{}; + Require(open_nid_postfix(blocked->c_str(), 0, 0) == -1 && *__error_nid_postfix() == expected); + Require(open_nid_postfix(blocked->c_str(), 0x202, 0644) == -1 && *__error_nid_postfix() == expected); + Require(stat_nid_postfix(blocked->c_str(), &blockedStatus) == -1 && *__error_nid_postfix() == expected); + Require(lstat_nid_postfix(blocked->c_str(), &blockedStatus) == -1 && *__error_nid_postfix() == expected); + Require(access_nid_postfix(blocked->c_str(), 0) == -1 && *__error_nid_postfix() == expected); + Require(unlink_nid_postfix(blocked->c_str()) == -1 && *__error_nid_postfix() == expected); + Require(chmod_nid_postfix(blocked->c_str(), 0644) == -1 && *__error_nid_postfix() == expected); + Require(sceKernelTruncate_nid_postfix(blocked->c_str(), 0) == static_cast(0x80020000u | static_cast(expected))); + Require(sceKernelUtimes_nid_postfix(blocked->c_str(), nullptr) == static_cast(0x80020000u | static_cast(expected))); + Require(rename_nid_postfix(blocked->c_str(), (root / "renamed-target").string().c_str()) == -1 && *__error_nid_postfix() == expected); + if (blocked != &missingChild) Require(rename_nid_postfix(presentName.c_str(), blocked->c_str()) == -1 && *__error_nid_postfix() == expected); + Require(remove_nid_postfix(blocked->c_str()) == -1 && *__error_nid_postfix() == expected); + Require(opendir_nid_postfix(blocked->c_str()) == nullptr && *__error_nid_postfix() == expected); + Require(chdir_nid_postfix(blocked->c_str()) == -1 && *__error_nid_postfix() == expected); + for (const char* openMode : {"r", "w"}) { + Require(fopen_nid_postfix(blocked->c_str(), openMode) == nullptr && *__error_nid_postfix() == expected); + } + } #ifndef _WIN32 const auto linkedDirectory = root / "linked-directory"; const auto directoryLink = root / "directory-link"; diff --git a/core/libs/tests/GuestGameUpdate.cpp b/core/libs/tests/GuestGameUpdate.cpp index 0394f1283d..13fcac85c2 100644 --- a/core/libs/tests/GuestGameUpdate.cpp +++ b/core/libs/tests/GuestGameUpdate.cpp @@ -3,16 +3,60 @@ #include #include #include +#include #include extern "C" { int APS5_VABI sceGameUpdateInitialize(void); int APS5_VABI sceGameUpdateTerminate(void); int APS5_VABI sceGameUpdateGetAddcontLatestVersion(std::uint32_t, const void*, GameUpdateAddcontVersionInfo*); +int APS5_VABI sceGameUpdateCreateRequest(void); +int APS5_VABI sceGameUpdateCheck(int, const GameUpdateCheckParam*, GameUpdateCheckResult*); +int APS5_VABI sceGameUpdateAbortRequest(int); +int APS5_VABI sceGameUpdateDeleteRequest(int); } static void Require(bool value) { if (!value) std::abort(); } +static void TestRequestArguments(int notInitialized, int invalidArgument, int invalidSize, int notFound) { + GameUpdateCheckParam param{}; + param.size = sizeof(param); + GameUpdateCheckResult result{}; + result.size = sizeof(result); + constexpr int requestId = 0x20000000; + + for (int id : {0, -1}) { + Require(sceGameUpdateCheck(id, nullptr, nullptr) == notFound); + Require(sceGameUpdateAbortRequest(id) == notFound); + Require(sceGameUpdateDeleteRequest(id) == notFound); + } + Require(sceGameUpdateCheck(requestId, nullptr, &result) == invalidArgument); + Require(sceGameUpdateCheck(requestId, ¶m, nullptr) == invalidArgument); + param.size = sizeof(param) - 1; + Require(sceGameUpdateCheck(requestId, ¶m, &result) == invalidSize); + param.size = sizeof(param); + result.size = sizeof(result) + 8; + Require(sceGameUpdateCheck(requestId, ¶m, &result) == invalidSize); + result.size = sizeof(result); + param.option = 1; + Require(sceGameUpdateCheck(requestId, ¶m, &result) == invalidArgument); + param.option = 0; + param.reserved[8] = 1; + Require(sceGameUpdateCheck(requestId, ¶m, &result) == invalidArgument); + param.reserved[8] = 0; + + Require(sceGameUpdateCreateRequest() == notInitialized); + Require(sceGameUpdateCheck(requestId, ¶m, &result) == notInitialized); + Require(sceGameUpdateAbortRequest(requestId) == notInitialized); + Require(sceGameUpdateDeleteRequest(requestId) == notInitialized); + + Require(sceGameUpdateInitialize() == 0); + Require(sceGameUpdateCheck(requestId, ¶m, &result) == notFound); + Require(sceGameUpdateAbortRequest(requestId) == notFound); + Require(sceGameUpdateDeleteRequest(requestId) == notFound); + Require(sceGameUpdateTerminate() == 0); +} + int main() { constexpr int notInitialized = static_cast(0x80412801); constexpr int invalidArgument = static_cast(0x80412803); @@ -23,8 +67,17 @@ int main() { GameUpdateAddcontVersionInfo info{}; info.size = sizeof(info); + constexpr int alreadyInitialized = static_cast(0x80412802); + constexpr int notFound = static_cast(0x80412805); + + Require(sceGameUpdateTerminate() == notInitialized); + TestRequestArguments(notInitialized, invalidArgument, invalidSize, notFound); + Require(sceGameUpdateGetAddcontLatestVersion(0, label, &info) == notInitialized); + Require(sceGameUpdateGetAddcontLatestVersion(0, label, nullptr) == invalidArgument); + Require(sceGameUpdateGetAddcontLatestVersion(0, nullptr, &info) == invalidArgument); Require(sceGameUpdateInitialize() == 0); + Require(sceGameUpdateInitialize() == alreadyInitialized); Require(sceGameUpdateGetAddcontLatestVersion(0, label, nullptr) == invalidArgument); Require(sceGameUpdateGetAddcontLatestVersion(0, nullptr, &info) == invalidArgument); @@ -40,6 +93,7 @@ int main() { for (std::size_t i = sizeof(info.size); i < sizeof(info); ++i) Require(bytes[i] == 0); Require(sceGameUpdateTerminate() == 0); + Require(sceGameUpdateTerminate() == notInitialized); info.size = sizeof(info); Require(sceGameUpdateGetAddcontLatestVersion(0, label, &info) == notInitialized); } diff --git a/core/libs/tests/GuestHeapTraceInfo.cpp b/core/libs/tests/GuestHeapTraceInfo.cpp new file mode 100644 index 0000000000..56c18baf06 --- /dev/null +++ b/core/libs/tests/GuestHeapTraceInfo.cpp @@ -0,0 +1,29 @@ +#include "prx/libc/include/HeapDiagnostics.hpp" +#include +#include +#include + +namespace { + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +} + +int main() { + LibcHeapInfo info{sizeof(LibcHeapInfo), 0, 1, nullptr, nullptr}; + LibcHeapTraceInfo_nid_no_patch(&info); + Require(info.unknown2 == 0, "heap trace info did not clear unknown2"); + Require(info.mspace_atomic_id_mask != nullptr, "heap trace info returned no mspace mask"); + Require(info.mstate_table != nullptr, "heap trace info returned no mstate table"); + + constexpr std::uint64_t first = 0x0123456789abcdefull; + constexpr std::uint64_t last = 0xfedcba9876543210ull; + info.mstate_table[0] = first; + info.mstate_table[64] = last; + Require(info.mstate_table[0] == first, "heap trace info base slot is not writable"); + Require(info.mstate_table[64] == last, "heap trace info final 64-bit mask slot is not writable"); +} diff --git a/core/libs/tests/GuestHmd2.cpp b/core/libs/tests/GuestHmd2.cpp index 556abe12ee..f9781e407b 100644 --- a/core/libs/tests/GuestHmd2.cpp +++ b/core/libs/tests/GuestHmd2.cpp @@ -63,7 +63,19 @@ void TestWithoutHeadset() { } +void SizeQueriesReturnTheConsoleSizes() { + const auto display = sceHmd2ReprojectionQueryDisplayBufferSizeAlign(); + Require(display.size == 0x6000000 && display.align == 0x10000, "display buffer size"); + const auto seeThrough = sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign(); + Require(seeThrough.size == 0x1106000 && seeThrough.align == 0x100, "see-through buffer size"); + const auto mirroring = sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign(); + Require(mirroring.size == 0x5FA0000 && mirroring.align == 0x10000, "mirroring display buffer size"); + const auto work = sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign(); + Require(work.size == 0x4000 && work.align == 0x100, "mirroring work memory size"); +} + int main() { + SizeQueriesReturnTheConsoleSizes(); InstallProcessParameters(); TestBeforeInitialize(); TestWithoutHeadset(); diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index bd3a9b01c9..cb35456d8c 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -50,6 +50,27 @@ static void Require(bool value) { if (!value) std::abort(); } static bool Equal(const char* left, const char* right) { return std::strcmp(left, right) == 0; } +static void CheckUriParseCharacters() { + char pool[256]; + SceHttpUriElement element{}; + Require(sceHttpUriParse(&element, "HtTpS://AZaz09-._/path", pool, nullptr, sizeof(pool)) == 0); + Require(Equal(element.scheme, "HtTpS") && Equal(element.hostname, "AZaz09-._") && element.port == 443); + Require(sceHttpUriParse(&element, "aZ09+.-://host/", pool, nullptr, sizeof(pool)) == 0); + Require(Equal(element.scheme, "aZ09+.-") && element.port == 0); + for (unsigned value = 128; value <= 255; ++value) { + const std::string byte(1, static_cast(value)); + const std::string urls[] = { + "http://" + byte + "/", "http://[" + byte + "]/", byte + "://host/", + "a" + byte + "://host/", "http://host:" + byte + "/" + }; + for (const auto& url : urls) { + std::size_t required = 0; + Require(sceHttpUriParse(nullptr, url.c_str(), nullptr, &required, 0) == static_cast(0x80433060)); + Require(sceHttpUriParse(&element, url.c_str(), pool, nullptr, sizeof(pool)) == static_cast(0x80433060)); + } + } +} + static void CheckUriEscapeBytes() { constexpr char unreserved[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; constexpr char hex[] = "0123456789ABCDEF"; @@ -163,13 +184,22 @@ int main() { Require(sceHttpUriEscape(escaped, nullptr, sizeof(escaped), "a b/~\xC3\xA9") == 0); Require(Equal(escaped, "a%20b%2F~%C3%A9")); Require(sceHttpUriEscape(escaped, nullptr, sizeof(escaped), nullptr) == invalidValue); - CheckUriEscapeBytes(); -#ifdef _WIN32 const std::string previousCtype = std::setlocale(LC_CTYPE, nullptr); + Require(std::setlocale(LC_CTYPE, "C") != nullptr); + CheckUriEscapeBytes(); + CheckUriParseCharacters(); +#ifdef _WIN32 Require(std::setlocale(LC_CTYPE, ".1252") != nullptr); CheckUriEscapeBytes(); - Require(std::setlocale(LC_CTYPE, previousCtype.c_str()) != nullptr); + CheckUriParseCharacters(); +#else + for (const char* locale : {"en_US.ISO8859-1", "en_US.iso88591", "en_US.UTF-8"}) { + if (!std::setlocale(LC_CTYPE, locale)) continue; + CheckUriEscapeBytes(); + CheckUriParseCharacters(); + } #endif + Require(std::setlocale(LC_CTYPE, previousCtype.c_str()) != nullptr); constexpr const char* unescapeCases[][2] = { {"", ""}, {"plain+text", "plain+text"}, {"a%20b%2F~%c3%a9", "a b/~\xC3\xA9"}, diff --git a/core/libs/tests/GuestImeOpen.cpp b/core/libs/tests/GuestImeOpen.cpp index 15029e736b..ce516d5530 100644 --- a/core/libs/tests/GuestImeOpen.cpp +++ b/core/libs/tests/GuestImeOpen.cpp @@ -73,6 +73,95 @@ void APS5_VABI Other(void*, const ImeEvent*) { std::abort(); } +int32_t APS5_VABI UpperFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + for (uint32_t i = 0; i < sourceTextLength; ++i) { + const char16_t unit = sourceText[i]; + outText[i] = unit >= u'a' && unit <= u'z' ? static_cast(unit - u'a' + u'A') : unit; + } + *outTextLength = sourceTextLength; + return 0; +} + +int32_t APS5_VABI FailingFilter(char16_t*, uint32_t*, const char16_t*, uint32_t) { + return 1; +} + +int32_t APS5_VABI StripXFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + uint32_t length = 0; + for (uint32_t i = 0; i < sourceTextLength; ++i) { + if (sourceText[i] != u'x') outText[length++] = sourceText[i]; + } + *outTextLength = length; + return 0; +} + +int32_t APS5_VABI InvalidOutputFilter(char16_t* outText, uint32_t* outTextLength, const char16_t*, uint32_t) { + outText[0] = 0xdc00; + *outTextLength = 1; + return 0; +} + +int32_t APS5_VABI TooLongFilter(char16_t*, uint32_t* outTextLength, const char16_t*, uint32_t) { + *outTextLength = 9; + return 0; +} + +int32_t APS5_VABI ShortenFilter(char16_t* outText, uint32_t* outTextLength, const char16_t*, uint32_t) { + outText[0] = u'a'; + *outTextLength = 1; + return 0; +} + +int32_t APS5_VABI PairAtCaretFilter(char16_t* outText, uint32_t* outTextLength, const char16_t*, uint32_t) { + outText[0] = u'a'; + outText[1] = u'b'; + outText[2] = 0xd83d; + outText[3] = 0xde00; + *outTextLength = 4; + return 0; +} + +int APS5_VABI ExtNoop(const ImeKeycode*, std::uint16_t*, std::uint32_t*, void*) { + return 0; +} + +bool g_sawCaptureInsideFilter = false; +int g_setInsideRc = -1; + +int32_t APS5_VABI LockingFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + g_sawCaptureInsideFilter = ImeCapturesKeyboard_nid_postfix(); + for (uint32_t i = 0; i < sourceTextLength; ++i) { + outText[i] = sourceText[i]; + } + *outTextLength = sourceTextLength; + return 0; +} + +int32_t APS5_VABI SetInsideFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + g_setInsideRc = sceImeSetText(u"zz", 2); + for (uint32_t i = 0; i < sourceTextLength; ++i) { + outText[i] = sourceText[i]; + } + *outTextLength = sourceTextLength; + return 0; +} + +bool g_caretInsideFirstCall = true; +int g_caretInsideRc = -1; + +int32_t APS5_VABI SetCaretInsideFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + if (g_caretInsideFirstCall) { + g_caretInsideFirstCall = false; + const Caret caret{0.0f, 0.0f, 0, 0}; + g_caretInsideRc = sceImeSetCaret(&caret); + } + for (uint32_t i = 0; i < sourceTextLength; ++i) { + outText[i] = sourceText[i]; + } + *outTextLength = sourceTextLength; + return 0; +} + void Require(bool value, const char* message) { if (value) return; std::fprintf(stderr, "%s\n", message); @@ -174,15 +263,17 @@ void CheckValidation() { Require(sceImeClose_nid_postfix() == 0, "close after over-2K open"); param = MakeParam(buffers, 16); - param.filter = reinterpret_cast(&Record); - bool threw = false; - try { - sceImeOpen_nid_postfix(¶m, nullptr); - } catch (const std::exception&) { - threw = true; - } - Require(threw, "text filter must throw"); - Require(!ImeCapturesKeyboard_nid_postfix(), "failed open left a session"); + param.filter = reinterpret_cast(&UpperFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a text filter"); + Require(ImeCapturesKeyboard_nid_postfix(), "text filter session not open"); + Require(sceImeClose_nid_postfix() == 0, "close the text filter session"); + + param = MakeParam(buffers, 16); + ExtendedParam extFilters{}; + extFilters.ext_keyboard_filter = reinterpret_cast(&ExtNoop); + Require(sceImeOpen_nid_postfix(¶m, &extFilters) == 0, "open with an external keyboard filter"); + Require(ImeCapturesKeyboard_nid_postfix(), "external keyboard filter session not open"); + Require(sceImeClose_nid_postfix() == 0, "close the external keyboard filter session"); } void CheckEditing() { @@ -304,6 +395,141 @@ void CheckMultiline() { Require(sceImeClose_nid_postfix() == 0, "multiline close"); } +void CheckTextFilter() { + Buffers buffers; + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + Param param = MakeParam(buffers, 8); + param.filter = reinterpret_cast(&UpperFilter); + ExtendedParam extFilters{}; + extFilters.ext_keyboard_filter = reinterpret_cast(&ExtNoop); + Require(sceImeOpen_nid_postfix(¶m, &extFilters) == 0, "open with both filters"); + Drain(); + + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"ABC", 3, 0, 1); + Require(std::u16string(buffers.text) == u"ABC", "filtered input buffer"); + Require(std::u16string(reinterpret_cast(buffers.work)) == u"ABC", "filtered work copy"); + + Publish(ImeHostInputKind::Backspace); + RequireText(Drain().at(0), EventUpdateText, u"AB", 2, 2, -1); + + Publish(ImeHostInputKind::Left); + RequireCaretSteps(Drain(), 1, 1); + Publish(ImeHostInputKind::Delete); + RequireText(Drain().at(0), EventUpdateText, u"A", 1, 1, -1); + + Require(sceImeSetText(u"xy", 2) == 0, "set text with a filter"); + Require(std::u16string(buffers.text) == u"xy", "set text is not filtered"); + Require(Drain().empty(), "set text with a filter queued an event"); + Require(sceImeClose_nid_postfix() == 0, "close the filtered session"); + + buffers = Buffers{}; + param = MakeParam(buffers, 8); + param.filter = reinterpret_cast(&StripXFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a stripping filter"); + Drain(); + Publish(ImeHostInputKind::Text, "x"); + Require(Drain().empty(), "a stripped edit sends no event"); + Require(std::u16string(buffers.text).empty(), "a stripped edit leaves the buffers"); + Publish(ImeHostInputKind::Text, "b"); + RequireText(Drain().at(0), EventUpdateText, u"b", 1, 0, 1); + Publish(ImeHostInputKind::Text, "x"); + Require(Drain().empty(), "a stripped edit sends no event again"); + Require(std::u16string(buffers.text) == u"b", "buffers after a stripped edit"); + Require(sceImeClose_nid_postfix() == 0, "close the stripping session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = reinterpret_cast(&FailingFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a failing filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"abc", 3, 2, 1); + Require(std::u16string(buffers.text) == u"abc", "a failed filter keeps the edit"); + Require(sceImeClose_nid_postfix() == 0, "close the failing session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = reinterpret_cast(&InvalidOutputFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with an invalid-output filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"abc", 3, 2, 1); + Require(std::u16string(buffers.text) == u"abc", "an invalid filter output keeps the edit"); + Require(sceImeClose_nid_postfix() == 0, "close the invalid-output session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = reinterpret_cast(&TooLongFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a too-long filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"abc", 3, 2, 1); + Require(std::u16string(buffers.text) == u"abc", "an overlong filter output keeps the edit"); + Require(sceImeClose_nid_postfix() == 0, "close the too-long session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = reinterpret_cast(&ShortenFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a shortening filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"a", 1, 1, -1); + Require(sceImeClose_nid_postfix() == 0, "close the shortening session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = reinterpret_cast(&PairAtCaretFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a pair filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"ab\U0001F600", 4, 2, 2); + Require(sceImeClose_nid_postfix() == 0, "close the pair session"); + + buffers = Buffers{}; + param = MakeParam(buffers, 8); + param.filter = reinterpret_cast(&LockingFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a filter that calls sceIme*"); + Drain(); + g_sawCaptureInsideFilter = false; + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"c", 1, 0, 1); + Require(g_sawCaptureInsideFilter, "the filter can call back into sceIme*"); + Require(sceImeClose_nid_postfix() == 0, "close the locking session"); + + buffers = Buffers{}; + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = reinterpret_cast(&SetInsideFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a filter that sets the text"); + Drain(); + g_setInsideRc = -1; + Publish(ImeHostInputKind::Text, "c"); + Require(Drain().empty(), "an edit whose filter set the text sends no event"); + Require(g_setInsideRc == 0, "sceImeSetText inside the filter returns 0"); + Require(std::u16string(buffers.text) == u"zz", "the setText inside the filter wins"); + Require(std::u16string(reinterpret_cast(buffers.work)) == u"zz", "the setText inside the filter wins in the work copy"); + Require(sceImeClose_nid_postfix() == 0, "close the set-inside session"); + + buffers = Buffers{}; + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = reinterpret_cast(&SetCaretInsideFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a filter that moves the caret"); + Drain(); + g_caretInsideFirstCall = true; + g_caretInsideRc = -1; + Publish(ImeHostInputKind::Text, "c"); + Require(Drain().empty(), "an edit whose filter moved the caret sends no event"); + Require(g_caretInsideRc == 0, "sceImeSetCaret inside the filter returns 0"); + Require(std::u16string(buffers.text) == u"ab", "the discarded edit is not in the buffer"); + Publish(ImeHostInputKind::Text, "d"); + RequireText(Drain().at(0), EventUpdateText, u"dab", 1, 0, 1); + Require(std::u16string(buffers.text) == u"dab", "the edit after the discard builds on the old text"); + Require(std::u16string(reinterpret_cast(buffers.work)) == u"dab", "the edit after the discard reaches the work copy"); + Require(sceImeClose_nid_postfix() == 0, "close the caret-inside session"); +} + void CheckHostEvents() { Buffers buffers; Param param = MakeParam(buffers, 16); @@ -346,5 +572,6 @@ int main() { CheckValidation(); CheckEditing(); CheckMultiline(); + CheckTextFilter(); CheckHostEvents(); } diff --git a/core/libs/tests/GuestIntegerBuiltins.cpp b/core/libs/tests/GuestIntegerBuiltins.cpp index a347bc0c22..64f5cbb0e1 100644 --- a/core/libs/tests/GuestIntegerBuiltins.cpp +++ b/core/libs/tests/GuestIntegerBuiltins.cpp @@ -95,6 +95,23 @@ static void CheckShifts() { Require(__ashrti3_nid_postfix(Int128Min, 127) == -1); Require(__lshrti3_nid_postfix(Int128Min, 127) == 1); Require(static_cast(__lshrti3_nid_postfix(static_cast(Make(0x8000000000000001ull, 0)), 64)) == Make(0, 0x8000000000000001ull)); + Require(__ashldi3_nid_postfix(12345, 0) == 12345); + Require(__ashrdi3_nid_postfix(-12345, 0) == -12345); + Require(__lshrdi3_nid_postfix(INT64_MIN, 0) == INT64_MIN); + Require(__ashlti3_nid_postfix(Int128Max, 0) == Int128Max); + Require(__ashrti3_nid_postfix(Int128Min, 0) == Int128Min); + Require(__lshrti3_nid_postfix(Int128Max, 0) == Int128Max); + Require(__ashldi3_nid_postfix(0, 10) == 0); + Require(__ashrdi3_nid_postfix(0, 10) == 0); + Require(__lshrdi3_nid_postfix(0, 10) == 0); + Require(__ashlti3_nid_postfix(0, 70) == 0); + Require(__ashrti3_nid_postfix(0, 70) == 0); + Require(__lshrti3_nid_postfix(0, 70) == 0); + Require(static_cast(__ashlti3_nid_postfix(1, 64)) == Make(1, 0)); + Require(static_cast(__ashlti3_nid_postfix(1, 127)) == Make(1ull << 63, 0)); + Require(__lshrti3_nid_postfix(static_cast(Make(1, 0)), 64) == 1); + Require(__lshrti3_nid_postfix(Int128Min, 127) == 1); + Require(__ashrti3_nid_postfix(Int128Min, 64) == static_cast(Make(~0ull, 1ull << 63))); } static void CheckBitCounts() { @@ -110,6 +127,22 @@ static void CheckBitCounts() { Require(__parityti2_nid_postfix(Make(1, 1)) == 0 && __parityti2_nid_postfix(Make(1, 0)) == 1); Require(__popcountsi2_nid_postfix(0xffffffffu) == 32 && __popcountdi2_nid_postfix(~0ull) == 64); Require(__popcountti2_nid_postfix(Make(~0ull, ~0ull)) == 128); + Require(__clzsi2_nid_postfix(0) == 32); + Require(__ctzsi2_nid_postfix(0) == 32); + Require(__popcountsi2_nid_postfix(0) == 0); + Require(__popcountdi2_nid_postfix(0) == 0); + Require(__popcountti2_nid_postfix(0) == 0); + Require(__paritysi2_nid_postfix(0) == 0); + Require(__paritydi2_nid_postfix(0) == 0); + Require(__parityti2_nid_postfix(0) == 0); + Require(__clzsi2_nid_postfix(0x80000000u) == 0); + Require(__clzdi2_nid_postfix(1ull << 63) == 0); + Require(__clzti2_nid_postfix(Make(1ull << 63, 0)) == 0); + Require(__ctzsi2_nid_postfix(1) == 0); + Require(__ctzdi2_nid_postfix(1) == 0); + Require(__ctzti2_nid_postfix(1) == 0); + Require(__ffsdi2_nid_postfix(1) == 1 && __ffsdi2_nid_postfix(1ull << 63) == 64); + Require(__ffsti2_nid_postfix(1) == 1 && __ffsti2_nid_postfix(Make(1ull << 63, 0)) == 128); } static void CheckComparisons() { @@ -117,6 +150,13 @@ static void CheckComparisons() { Require(__cmpti2_nid_postfix(Int128Min, 0) == 0 && __cmpti2_nid_postfix(Int128Max, 0) == 2); Require(__ucmpdi2_nid_postfix(~0ull, 0) == 2 && __ucmpdi2_nid_postfix(0, 1) == 0); Require(__ucmpti2_nid_postfix(Make(1, 0), Make(0, ~0ull)) == 2 && __ucmpti2_nid_postfix(3, 3) == 1); + Require(__cmpdi2_nid_postfix(INT64_MIN, INT64_MIN) == 1 && __cmpdi2_nid_postfix(INT64_MAX, INT64_MAX) == 1); + Require(__cmpdi2_nid_postfix(INT64_MIN, INT64_MAX) == 0 && __cmpdi2_nid_postfix(INT64_MAX, INT64_MIN) == 2); + Require(__cmpti2_nid_postfix(Int128Min, Int128Min) == 1 && __cmpti2_nid_postfix(Int128Max, Int128Max) == 1); + Require(__cmpti2_nid_postfix(Int128Min, Int128Max) == 0 && __cmpti2_nid_postfix(-1, 1) == 0 && __cmpti2_nid_postfix(1, -1) == 2); + Require(__ucmpdi2_nid_postfix(~0ull, ~0ull) == 1 && __ucmpdi2_nid_postfix(0, 0) == 1); + Require(__ucmpti2_nid_postfix(0, 0) == 1 && __ucmpti2_nid_postfix(Make(~0ull, ~0ull), Make(~0ull, ~0ull)) == 1); + Require(__ucmpti2_nid_postfix(0, Make(~0ull, ~0ull)) == 0 && __ucmpti2_nid_postfix(Make(0, ~0ull), Make(1, 0)) == 0); } static void CheckArithmetic() { @@ -126,6 +166,17 @@ static void CheckArithmetic() { Require(static_cast(__multi3_nid_postfix(static_cast(Make(0, ~0ull)), static_cast(Make(0, ~0ull)))) == Make(0xfffffffffffffffeull, 1)); Require(__multi3_nid_postfix(-3, static_cast(Make(1, 0))) == -static_cast(Make(3, 0))); + Require(__negdi2_nid_postfix(0) == 0 && __negdi2_nid_postfix(1) == -1 && __negdi2_nid_postfix(-1) == 1); + Require(__negti2_nid_postfix(0) == 0 && __negti2_nid_postfix(1) == -1 && __negti2_nid_postfix(-1) == 1); + Require(__negti2_nid_postfix(Int128Max) == -Int128Max); + Require(__muldi3_nid_postfix(0, 100) == 0 && __muldi3_nid_postfix(100, 0) == 0); + Require(__muldi3_nid_postfix(1, INT64_MIN) == INT64_MIN && __muldi3_nid_postfix(1, INT64_MAX) == INT64_MAX); + Require(__muldi3_nid_postfix(-1, INT64_MAX) == -INT64_MAX && __muldi3_nid_postfix(-1, INT64_MIN) == INT64_MIN); + Require(__muldi3_nid_postfix(INT64_MAX, 2) == -2); + Require(__multi3_nid_postfix(0, Int128Max) == 0 && __multi3_nid_postfix(Int128Min, 0) == 0); + Require(__multi3_nid_postfix(1, Int128Min) == Int128Min && __multi3_nid_postfix(1, Int128Max) == Int128Max); + Require(__multi3_nid_postfix(-1, Int128Max) == -Int128Max && __multi3_nid_postfix(-1, Int128Min) == Int128Min); + Require(__multi3_nid_postfix(Int128Max, 2) == -2 && __multi3_nid_postfix(Int128Min, 2) == 0); } static void CheckDivision() { @@ -151,6 +202,18 @@ static void CheckDivision() { RequireThrows([] { __udivdi3_nid_postfix(1, 0); }); RequireThrows([] { __umoddi3_nid_postfix(1, 0); }); RequireThrows([] { __udivmodti4_nid_postfix(1, 0, nullptr); }); + Require(__divsi3_nid_postfix(INT32_MIN, -1) == INT32_MIN && __modsi3_nid_postfix(INT32_MIN, -1) == 0); + Require(__divsi3_nid_postfix(0, 5) == 0 && __modsi3_nid_postfix(0, 5) == 0); + Require(__divdi3_nid_postfix(0, 5) == 0 && __moddi3_nid_postfix(0, 5) == 0); + Require(__divmodsi4_nid_postfix(INT32_MIN, -1, &remainder32) == INT32_MIN && remainder32 == 0); + Require(__divmoddi4_nid_postfix(INT64_MIN, -1, &remainder64) == INT64_MIN && remainder64 == 0); + Require(__udivsi3_nid_postfix(0, 5) == 0 && __umodsi3_nid_postfix(0, 5) == 0); + Require(__udivdi3_nid_postfix(0, 5) == 0 && __umoddi3_nid_postfix(0, 5) == 0); + Require(__udivmodsi4_nid_postfix(0, 5, &uremainder32) == 0 && uremainder32 == 0); + Require(__udivmoddi4_nid_postfix(0, 5, &uremainder64) == 0 && uremainder64 == 0); + Require(__udivmodti4_nid_postfix(0, 5, &remainder128) == 0 && remainder128 == 0); + Require(__udivmodti4_nid_postfix(Make(~0ull, ~0ull), Make(~0ull, ~0ull), &remainder128) == 1 && remainder128 == 0); + Require(__udivmodti4_nid_postfix(Make(0, 10), Make(1, 0), &remainder128) == 0 && remainder128 == 10); } static void CheckOverflow() { @@ -181,6 +244,15 @@ static void CheckOverflow() { RequireThrows([] { __negvsi2_nid_postfix(INT32_MIN); }); RequireThrows([] { __absvdi2_nid_postfix(INT64_MIN); }); RequireThrows([] { __absvti2_nid_postfix(Int128Min); }); + Require(__mulosi4_nid_postfix(INT32_MIN, 1, &overflow) == INT32_MIN && overflow == 0); + Require(__mulosi4_nid_postfix(INT32_MIN, -1, &overflow) == INT32_MIN && overflow == 1); + Require(__mulosi4_nid_postfix(0, INT32_MIN, &overflow) == 0 && overflow == 0); + Require(__mulodi4_nid_postfix(0, INT64_MIN, &overflow) == 0 && overflow == 0); + Require(__muloti4_nid_postfix(Int128Min, 1, &overflow) == Int128Min && overflow == 0); + Require(__muloti4_nid_postfix(Int128Min, -1, &overflow) == Int128Min && overflow == 1); + Require(__muloti4_nid_postfix(0, Int128Min, &overflow) == 0 && overflow == 0); + Require(__negvsi2_nid_postfix(0) == 0 && __negvdi2_nid_postfix(0) == 0 && __negvti2_nid_postfix(0) == 0); + Require(__absvsi2_nid_postfix(0) == 0 && __absvdi2_nid_postfix(0) == 0 && __absvti2_nid_postfix(0) == 0); } int main() { diff --git a/core/libs/tests/GuestInterfaces.cpp b/core/libs/tests/GuestInterfaces.cpp index 4a7202cf4e..5b69f12016 100644 --- a/core/libs/tests/GuestInterfaces.cpp +++ b/core/libs/tests/GuestInterfaces.cpp @@ -4,8 +4,17 @@ #include #include #else +#include #include +#include +#include #include +#include +#include +#include +#include +#include +#include #endif #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -46,6 +55,12 @@ struct HostInterfaceAddress { std::uint8_t address[16]; std::uint8_t netmask[16]; std::uint32_t scope; + char name[16]; + unsigned int index; + std::uint8_t type; + std::uint8_t hardwareLength; + std::uint8_t hardware[8]; + bool loopbackUp; }; static HostInterfaceAddress HostAddress(const sockaddr* address) { @@ -61,6 +76,43 @@ static HostInterfaceAddress HostAddress(const sockaddr* address) { return entry; } +#ifndef _WIN32 +static std::uint64_t SysfsCounter(const char* name, const char* file) { + std::ifstream input(std::string("/sys/class/net/") + name + "/statistics/" + file); + unsigned long long value = 0; + Require(static_cast(input >> value)); + return value; +} + +static std::uint32_t HostMtu(const char* name) { + const int probe = ::socket(AF_INET, SOCK_DGRAM, 0); + Require(probe >= 0); + ifreq request{}; + std::strncpy(request.ifr_name, name, sizeof(request.ifr_name) - 1); + Require(::ioctl(probe, SIOCGIFMTU, &request) == 0); + ::close(probe); + return static_cast(request.ifr_mtu); +} + +static void SendLoopbackDatagram() { + const int receiver = ::socket(AF_INET, SOCK_DGRAM, 0); + const int sender = ::socket(AF_INET, SOCK_DGRAM, 0); + Require(receiver >= 0 && sender >= 0); + sockaddr_in address{}; + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + Require(::bind(receiver, reinterpret_cast(&address), sizeof(address)) == 0); + socklen_t length = sizeof(address); + Require(::getsockname(receiver, reinterpret_cast(&address), &length) == 0); + const char payload = 'x'; + Require(::sendto(sender, &payload, 1, 0, reinterpret_cast(&address), sizeof(address)) == 1); + char received = 0; + Require(::recv(receiver, &received, 1, 0) == 1); + ::close(sender); + ::close(receiver); +} +#endif + static std::vector HostSnapshot() { std::vector entries; #ifdef _WIN32 @@ -88,22 +140,47 @@ static std::vector HostSnapshot() { #else ifaddrs* host = nullptr; Require(::getifaddrs(&host) == 0); + std::vector names; + std::vector> groups; + const auto group = [&](const char* name) -> std::vector& { + for (std::size_t index = 0; index < names.size(); ++index) + if (names[index] == name) return groups[index]; + names.emplace_back(name); + return groups.emplace_back(); + }; try { for (auto* item = host; item; item = item->ifa_next) { - if (!item->ifa_addr || (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6)) continue; + if (!item->ifa_addr) continue; + if (item->ifa_addr->sa_family == AF_PACKET) { + const auto& link = *reinterpret_cast(item->ifa_addr); + HostInterfaceAddress entry{}; + entry.family = AF_PACKET; + std::strncpy(entry.name, item->ifa_name, sizeof(entry.name) - 1); + entry.index = static_cast(link.sll_ifindex); + entry.type = link.sll_hatype == ARPHRD_ETHER ? 0x6 : link.sll_hatype == ARPHRD_LOOPBACK ? 0x18 : link.sll_hatype == ARPHRD_PPP ? 0x17 : 0x1; + entry.hardwareLength = entry.type == 0x18 ? 0 : link.sll_halen; + std::memcpy(entry.hardware, link.sll_addr, entry.hardwareLength); + entry.loopbackUp = (item->ifa_flags & IFF_UP) != 0; + auto& members = group(item->ifa_name); + members.insert(members.begin(), entry); + continue; + } + if (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6) continue; Require(item->ifa_netmask != nullptr); HostInterfaceAddress entry = HostAddress(item->ifa_addr); + std::strncpy(entry.name, item->ifa_name, sizeof(entry.name) - 1); if (entry.family == AF_INET) std::memcpy(entry.netmask, &reinterpret_cast(item->ifa_netmask)->sin_addr, 4); else std::memcpy(entry.netmask, &reinterpret_cast(item->ifa_netmask)->sin6_addr, 16); - entries.push_back(entry); + group(item->ifa_name).push_back(entry); } } catch (...) { ::freeifaddrs(host); throw; } ::freeifaddrs(host); + for (const auto& members : groups) entries.insert(entries.end(), members.begin(), members.end()); #endif return entries; } @@ -115,6 +192,10 @@ int main() { const std::uint8_t loopbackAddress[] = {127, 0, 0, 1}; const std::uint8_t loopbackMask[] = {255, 0, 0, 0}; bool matched = false; +#ifndef _WIN32 + bool linkChecked = false; + SendLoopbackDatagram(); +#endif for (unsigned attempt = 0; attempt < 3 && !matched; ++attempt) { const std::vector host = HostSnapshot(); GuestInterfaceAddress* list = nullptr; @@ -128,8 +209,47 @@ int main() { const std::size_t nameLength = std::strlen(item->name); Require(nameLength >= 1 && nameLength <= 15); Require((item->flags & ~knownFlags) == 0); - Require(item->data == nullptr); const auto* address = static_cast(item->address); + Require(address != nullptr && reinterpret_cast(address) % 8 == 0); +#ifndef _WIN32 + if (address[1] == 18) { + Require(item->netmask == nullptr && item->destination == nullptr); + const auto* data = static_cast(item->data); + Require(data != nullptr && reinterpret_cast(data) % 8 == 0); + std::uint16_t index = 0; + std::uint16_t dataLength = 0; + std::uint32_t mtu = 0; + std::uint64_t counters[11] = {}; + std::memcpy(&index, address + 2, 2); + std::memcpy(&dataLength, data + 6, 2); + std::memcpy(&mtu, data + 8, 4); + std::memcpy(counters, data + 24, sizeof(counters)); + Require(address[0] >= 54 && address[0] % 8 == 0 && address[0] >= 24 + address[6]); + Require(address[5] == nameLength && std::memcmp(address + 8, item->name, nameLength) == 0 && address[7] == 0); + Require(dataLength == 152 && data[0] == address[4] && data[2] == address[6] && data[1] == 0 && data[5] == 0); + Require(data[4] == 1 || data[4] == 2); + Require(index == if_nametoindex(item->name)); + Require(mtu == HostMtu(item->name)); + Require(counters[0] <= SysfsCounter(item->name, "rx_packets") && counters[2] <= SysfsCounter(item->name, "tx_packets")); + Require(counters[5] <= SysfsCounter(item->name, "rx_bytes") && counters[6] <= SysfsCounter(item->name, "tx_bytes")); + if (entries <= host.size()) { + const HostInterfaceAddress& expected = host[entries - 1]; + equal = equal && expected.family == AF_PACKET && std::strcmp(expected.name, item->name) == 0 && expected.index == index && + expected.type == address[4] && expected.hardwareLength == address[6] && + std::memcmp(expected.hardware, address + 8 + nameLength, expected.hardwareLength) == 0; + if (expected.type == 0x18) { + Require(data[3] == 0 && data[4] == (expected.loopbackUp ? 2 : 1)); + if (expected.loopbackUp) Require(counters[0] > 0 && counters[2] > 0 && counters[5] > 0 && counters[6] > 0); + linkChecked = linkChecked || expected.loopbackUp; + } + if (expected.type == 0x6) Require(data[3] == 14 && address[6] == 6); + } else { + equal = false; + } + continue; + } +#endif + Require(item->data == nullptr); const auto* netmask = static_cast(item->netmask); Require(address != nullptr && netmask != nullptr); Require(reinterpret_cast(address) % 8 == 0); @@ -151,6 +271,9 @@ int main() { if (item->flags & 0x10) Require(item->destination != nullptr); if (entries <= host.size()) { const HostInterfaceAddress& expected = host[entries - 1]; +#ifndef _WIN32 + equal = equal && std::strcmp(expected.name, item->name) == 0; +#endif const bool ipv4 = address[1] == 2; const std::size_t offset = ipv4 ? 4 : 8; const std::size_t length = ipv4 ? 4 : 16; @@ -172,6 +295,9 @@ int main() { freeifaddrs_nid_postfix(list); } Require(matched); +#ifndef _WIN32 + Require(linkChecked); +#endif GuestInterfaceAddress* released = nullptr; Require(getifaddrs_nid_postfix(&released) == 0 && released != nullptr); free_nid_postfix(released); diff --git a/core/libs/tests/GuestJpegDec.cpp b/core/libs/tests/GuestJpegDec.cpp index 0d9e9dcf43..8dfa4cb137 100644 --- a/core/libs/tests/GuestJpegDec.cpp +++ b/core/libs/tests/GuestJpegDec.cpp @@ -1,6 +1,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "Decoder/Jpeg.hpp" +#include #include #include #include @@ -80,8 +81,19 @@ int main() { Require(sceJpegDecParseHeader(&parseParam, nullptr) == invalidAddr); const JpegDecParseParam emptyParam{nullptr, 0, 0, 1}; Require(sceJpegDecParseHeader(&emptyParam, &info) == invalidAddr); - const JpegDecParseParam downScaledParse{jpeg.data(), static_cast(jpeg.size()), 0, 2}; - Require(ThrowsRuntimeError([&] { sceJpegDecParseHeader(&downScaledParse, &info); })); + for (const std::uint16_t downScale : {std::uint16_t{0}, std::uint16_t{3}, std::uint16_t{16}}) { + const JpegDecParseParam unknownScaleParse{jpeg.data(), static_cast(jpeg.size()), 0, downScale}; + Require(ThrowsRuntimeError([&] { sceJpegDecParseHeader(&unknownScaleParse, &info); })); + } + const JpegDecParseParam otherModeParse{jpeg.data(), static_cast(jpeg.size()), 1, 1}; + Require(ThrowsRuntimeError([&] { sceJpegDecParseHeader(&otherModeParse, &info); })); + for (const auto [downScale, outputWidth, outputHeight] : {std::array{2, 4, 3}, std::array{4, 2, 2}, std::array{8, 1, 1}}) { + const JpegDecParseParam downScaledParse{jpeg.data(), static_cast(jpeg.size()), 0, static_cast(downScale)}; + info = {}; + Require(sceJpegDecParseHeader(&downScaledParse, &info) == 0); + Require(info.image_width == 7 && info.image_height == 5 && info.component_number == 3); + Require(info.output_image_width == outputWidth && info.output_image_height == outputHeight); + } const std::vector grayJpeg = TestJpeg(3, 2, 1); const JpegDecParseParam grayParse{grayJpeg.data(), static_cast(grayJpeg.size()), 0, 1}; Require(sceJpegDecParseHeader(&grayParse, &info) == 0); @@ -125,9 +137,9 @@ int main() { JpegDecDecodeParam badFormat = decodeParam; badFormat.pixel_format = 2; Require(sceJpegDecDecode(handle, &badFormat, &info) == invalidParam); - JpegDecDecodeParam downScaled = decodeParam; - downScaled.down_scale = 2; - Require(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &downScaled, &info); })); + JpegDecDecodeParam unknownScale = decodeParam; + unknownScale.down_scale = 3; + Require(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &unknownScale, &info); })); JpegDecDecodeParam otherMode = decodeParam; otherMode.decode_mode = 1; Require(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &otherMode, &info); })); @@ -157,12 +169,63 @@ int main() { for (std::size_t x = 7 * 4; x < pitch; ++x) Require(bgra[y * pitch + x] == 0xEE); } + const std::vector large = TestJpeg(37, 21, 3); + const std::vector largeGray = TestJpeg(19, 11, 1); + for (const auto* source : {&large, &largeGray}) { + const std::optional sourceHeader = Decoder::Jpeg::ParseHeader(*source); + Require(sourceHeader.has_value()); + for (const std::uint16_t downScale : {std::uint16_t{2}, std::uint16_t{4}, std::uint16_t{8}}) { + const std::uint32_t width = (sourceHeader->width + downScale - 1) / downScale; + const std::uint32_t height = (sourceHeader->height + downScale - 1) / downScale; + const std::optional scaled = Decoder::Jpeg::DecodeScaled(*source, downScale); + Require(scaled && scaled->width == width && scaled->height == height); + + std::vector exact(static_cast(width) * height * 4, 0xCC); + JpegDecDecodeParam scaledParam = DecodeParam(*source, exact, 0); + scaledParam.down_scale = downScale; + info = {}; + Require(sceJpegDecDecode(handle, &scaledParam, &info) == static_cast(width << 16 | height)); + Require(info.image_width == sourceHeader->width && info.image_height == sourceHeader->height); + Require(info.output_image_width == width && info.output_image_height == height); + for (std::size_t pixel = 0; pixel < static_cast(width) * height; ++pixel) { + for (std::size_t c = 0; c < 3; ++c) Require(exact[pixel * 4 + c] == scaled->pixels[pixel * scaled->channels + (scaled->channels == 1 ? 0 : c)]); + Require(exact[pixel * 4 + 3] == 0xFF); + } + + const std::uint32_t paddedPitch = width * 4 + 4; + std::vector padded((height - 1) * paddedPitch + width * 4, 0xEE); + JpegDecDecodeParam paddedParam = DecodeParam(*source, padded, paddedPitch); + paddedParam.down_scale = downScale; + paddedParam.pixel_format = 1; + Require(sceJpegDecDecode(handle, &paddedParam, nullptr) == static_cast(width << 16 | height)); + for (std::size_t y = 0; y < height; ++y) { + for (std::size_t x = 0; x < width; ++x) { + const std::uint8_t* pixel = &padded[y * paddedPitch + x * 4]; + const std::uint8_t* rgba = &exact[(y * width + x) * 4]; + Require(pixel[0] == rgba[2] && pixel[1] == rgba[1] && pixel[2] == rgba[0] && pixel[3] == 0xFF); + } + if (y + 1 < height) for (std::size_t x = width * 4; x < paddedPitch; ++x) Require(padded[y * paddedPitch + x] == 0xEE); + } + + std::vector small(exact.size() - 1, 0); + JpegDecDecodeParam smallParam = DecodeParam(*source, small, 0); + smallParam.down_scale = downScale; + Require(sceJpegDecDecode(handle, &smallParam, &info) == invalidSize); + JpegDecDecodeParam narrowParam = DecodeParam(*source, exact, width * 4 - 1); + narrowParam.down_scale = downScale; + Require(sceJpegDecDecode(handle, &narrowParam, &info) == invalidParam); + } + } + const std::vector garbage{0x00, 0x01, 0x02, 0x03}; const JpegDecParseParam garbageParam{garbage.data(), static_cast(garbage.size()), 0, 1}; Require(sceJpegDecParseHeader(&garbageParam, &info) == invalidData); std::vector garbageOut(64, 0); const JpegDecDecodeParam garbageDecode = DecodeParam(garbage, garbageOut, 4 * 4); Require(sceJpegDecDecode(handle, &garbageDecode, &info) == invalidData); + JpegDecDecodeParam garbageScaled = garbageDecode; + garbageScaled.down_scale = 2; + Require(sceJpegDecDecode(handle, &garbageScaled, &info) == invalidData); Require(sceJpegDecDelete(handle) == 0); return 0; diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 77d2e43bad..339c3c4697 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -72,6 +72,9 @@ static constexpr std::uint64_t WindowSizeRequest = 0x40087468; static constexpr std::uint64_t AsyncRequest = 0x8004667d; static constexpr std::uint64_t CloseOnExecRequest = 0x20006601; static constexpr std::uint64_t KeepOnExecRequest = 0x20006602; +static constexpr std::uint64_t MalformedRequests[] = {0, 123, 0x5401, 0x5413, 0x40007468, 0x20087468, 0x00087468}; +static constexpr std::uint64_t IntegerArgumentRequest = 0x20046601; +static constexpr std::uint64_t EmptyInputRequest = 0x80006601; static constexpr int SceEbadf = static_cast(0x80020009u); static constexpr int SceEinval = static_cast(0x80020016u); static constexpr int SceEnotsup = static_cast(0x8002002du); @@ -99,6 +102,14 @@ template static bool ThrowsWith(TCall call, const std::string& return false; } +static void RequireMalformedRequestsFail(int descriptor) { + char buffer[16] = {}; + for (const std::uint64_t request : MalformedRequests) { + RequireFailure(ioctl_nid_postfix(descriptor, request, buffer), GuestEnotty); + RequireFailure(ioctl_nid_postfix(descriptor, request, nullptr), GuestEnotty); + } +} + static void SocketsKeepTheirPath() { const int socket = socket_nid_postfix(2, 2, 0); Require(socket >= 0); @@ -113,6 +124,9 @@ static void SocketsKeepTheirPath() { Require(sceKernelFcntl(socket + 4096, GetFlags) == SceEbadf); int queued = -1; Require(ioctl_nid_postfix(socket, ReadableRequest, &queued) == 0 && queued == 0); + RequireMalformedRequestsFail(socket); + RequireFailure(ioctl_nid_postfix(socket, IntegerArgumentRequest, nullptr), GuestEfault); + RequireFailure(ioctl_nid_postfix(socket, EmptyInputRequest, nullptr), GuestEfault); Require(close_nid_postfix(socket) == 0); Require(sceKernelFcntl(socket, GetFlags) == SceEbadf); Require(sceKernelFcntl(-1, GetFlags) == SceEbadf); @@ -164,6 +178,7 @@ static void FileFlagsRoundTrip() { Require(ioctl_nid_postfix(file, ReadableRequest, &readable) == 0 && readable == 3); char size[8] = {}; RequireFailure(ioctl_nid_postfix(file, WindowSizeRequest, size), GuestEnotty); + RequireMalformedRequestsFail(file); RequireFailure(ioctl_nid_postfix(file, NonblockingRequest, nullptr), GuestEfault); Require(ThrowsWith([file] { int on = 1; ioctl_nid_postfix(file, AsyncRequest, &on); }, "ioctl: request 0x8004667D on a file is not implemented")); @@ -188,6 +203,8 @@ static void FileFlagsRoundTrip() { RequireFailure(fcntl_nid_postfix(file, GetDescriptorFlags), GuestEbadf); RequireFailure(fcntl_nid_postfix(file, DuplicateCommand, 0), GuestEbadf); RequireFailure(ioctl_nid_postfix(file, ReadableRequest, &readable), GuestEbadf); + RequireMalformedRequestsFail(file); + RequireMalformedRequestsFail(-1); Require(sceKernelFcntl(file, GetFlags) == SceEbadf); const int appending = sceKernelOpen("kernel_fcntl_probe.bin", CreateWriteAppend, 0644); @@ -386,6 +403,7 @@ static void PipeFlagsRoundTrip() { RequireFailure(fcntl_nid_postfix(pipe[1], SetLock, &lock), GuestEbadf); char size[8] = {}; RequireFailure(ioctl_nid_postfix(pipe[0], WindowSizeRequest, size), GuestEnotty); + RequireMalformedRequestsFail(pipe[0]); Require(ThrowsWith([&pipe] { fcntl_nid_postfix(pipe[0], SetFlags, NonBlocking | 0x40); }, "fcntl: O_ASYNC on a pipe is not implemented")); for (const int descriptor : pipe) Require(close_nid_postfix(descriptor) == 0); } diff --git a/core/libs/tests/GuestKill.cpp b/core/libs/tests/GuestKill.cpp index 7a1f380d3c..7b75694ea5 100644 --- a/core/libs/tests/GuestKill.cpp +++ b/core/libs/tests/GuestKill.cpp @@ -32,7 +32,7 @@ int main() { Fails(target, 0, 3); Fails(target, 15, 3); } - for (int target : {self + 1, 1000}) { + for (int target : {self + 1, self + 1000}) { KillpgFails(target, 0, 3); KillpgFails(target, 15, 3); } diff --git a/core/libs/tests/GuestLibcCndTimedWait.cpp b/core/libs/tests/GuestLibcCndTimedWait.cpp index 467d2e6ed5..e85144f158 100644 --- a/core/libs/tests/GuestLibcCndTimedWait.cpp +++ b/core/libs/tests/GuestLibcCndTimedWait.cpp @@ -1,10 +1,18 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include #include #include #include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif struct Xtime { std::int64_t sec; @@ -18,12 +26,30 @@ int APS5_VABI _Mtx_lock_nid_postfix(void** handle); int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); int APS5_VABI _Cnd_init_nid_postfix(void** handle); void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); +int APS5_VABI _Cnd_wait_nid_postfix(void** condition, void** mutex); int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle); int APS5_VABI _Cnd_timedwait_nid_postfix(void** condition, void** mutex, const Xtime* target); } static void Require(bool value) { if (!value) std::abort(); } +#ifdef _WIN32 +struct TimerResolution { + using SetTimerResolution = LONG(WINAPI*)(ULONG, BOOLEAN, PULONG); + SetTimerResolution set = nullptr; + ULONG current = 0; + + TimerResolution() { + const HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); + if (ntdll) + set = reinterpret_cast(GetProcAddress(ntdll, "NtSetTimerResolution")); + Require(set != nullptr && set(5000, TRUE, ¤t) >= 0); + } + + ~TimerResolution() { set(5000, FALSE, ¤t); } +}; +#endif + template static void RequireThrows(TCall call) { try { @@ -72,6 +98,22 @@ int main() { Require(result == success); } +#ifdef _WIN32 + TimerResolution timerResolution; + std::vector shortWaits; + for (int i = 0; i < 31; ++i) { + const auto start = std::chrono::steady_clock::now(); + const auto deadline = std::chrono::system_clock::now() + std::chrono::milliseconds(1); + const auto seconds = std::chrono::duration_cast(deadline.time_since_epoch()).count(); + const auto nanoseconds = std::chrono::duration_cast(deadline.time_since_epoch()).count() % 1000000000; + const Xtime shortTimeout{seconds, nanoseconds}; + Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &shortTimeout) == timedout); + shortWaits.push_back(std::chrono::steady_clock::now() - start); + } + std::sort(shortWaits.begin(), shortWaits.end()); + Require(shortWaits[shortWaits.size() / 2] < std::chrono::milliseconds(12)); +#endif + const auto unnormalizedDeadline = std::chrono::system_clock::now() + std::chrono::milliseconds(20); const auto unnormalizedSec = std::chrono::duration_cast(unnormalizedDeadline.time_since_epoch()).count(); const auto unnormalizedNsec = std::chrono::duration_cast(unnormalizedDeadline.time_since_epoch()).count() % 1000000000; @@ -99,6 +141,18 @@ int main() { } signaler.join(); + ready = false; + std::thread untimedSignaler([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(_Mtx_lock_nid_postfix(&mutex) == success); + ready = true; + Require(_Cnd_broadcast_nid_postfix(&condition) == success); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + }); + Require(_Cnd_wait_nid_postfix(&condition, &mutex) == success); + Require(ready); + untimedSignaler.join(); + constexpr std::int64_t farSeconds = 10000000000; const Xtime farTime{farSeconds, 999999999}; constexpr auto clockSeconds = std::chrono::duration_cast(std::chrono::system_clock::duration::max()).count(); diff --git a/core/libs/tests/GuestLoginDialog.cpp b/core/libs/tests/GuestLoginDialog.cpp index 5fa3c659e7..0289d86fda 100644 --- a/core/libs/tests/GuestLoginDialog.cpp +++ b/core/libs/tests/GuestLoginDialog.cpp @@ -1,6 +1,9 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include +#include +#include extern "C" { int APS5_VABI sceLoginDialogInitialize(void); @@ -19,22 +22,37 @@ void Require(bool value) { if (!value) std::abort(); } constexpr int kStatusNone = 0; constexpr int kStatusInitialized = 1; constexpr int kStatusFinished = 3; -constexpr int kErrNotInitialized = static_cast(0x80B80003); -constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); -constexpr int kErrNotFinished = static_cast(0x80B80005); -constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kErrNotInitialized = static_cast(0x81340001); +constexpr int kErrAlreadyInitialized = static_cast(0x81340002); +constexpr int kErrParamInvalid = static_cast(0x81340003); +constexpr int kErrBusy = static_cast(0x81340005); +constexpr int kErrNoService = static_cast(0x81340007); constexpr int kResultUserCanceled = 1; +void RequireRejected(const std::uint32_t (&valid)[16], std::size_t word, std::uint32_t value) { + std::uint32_t param[16]; + std::memcpy(param, valid, sizeof(param)); + param[word] = value; + Require(sceLoginDialogOpen(param) == kErrParamInvalid); +} + +void RequireResult(const std::int32_t (&result)[4]) { + Require(result[0] == kResultUserCanceled); + Require(result[1] == kErrNoService); + Require(result[2] == -1 && result[3] == -1); +} + } int main() { - std::int64_t param = 0; - std::int32_t result[2] = {-1, -1}; + const std::uint32_t param[16] = {0x40}; + std::int32_t result[4] = {-1, -1, -1, -1}; Require(sceLoginDialogTerminate() == kErrNotInitialized); - Require(sceLoginDialogOpen(¶m) == kErrNotInitialized); + Require(sceLoginDialogOpen(param) == kErrNotInitialized); Require(sceLoginDialogClose() == kErrNotInitialized); Require(sceLoginDialogGetResult(result) == kErrNotInitialized); + Require(result[0] == -1 && result[1] == -1); Require(sceLoginDialogGetStatus() == kStatusNone); Require(sceLoginDialogUpdateStatus() == kStatusNone); @@ -43,17 +61,22 @@ int main() { Require(sceLoginDialogGetStatus() == kStatusInitialized); Require(sceLoginDialogUpdateStatus() == kStatusInitialized); - Require(sceLoginDialogOpen(nullptr) == kErrArgNull); - Require(sceLoginDialogGetResult(result) == kErrNotFinished); + Require(sceLoginDialogOpen(nullptr) == kErrParamInvalid); + for (std::uint32_t size : {0u, 0x3Cu, 0x3Fu, 0x41u}) RequireRejected(param, 0, size); + for (std::uint32_t selector : {2u, 0xFFFFFFFFu}) RequireRejected(param, 1, selector); + for (std::size_t word = 0x2C / 4; word < 16; ++word) RequireRejected(param, word, 1); + Require(sceLoginDialogGetStatus() == kStatusInitialized); + Require(sceLoginDialogGetResult(result) == kErrBusy); + RequireResult(result); + std::memset(result, 0xFF, sizeof(result)); - Require(sceLoginDialogOpen(¶m) == 0); + Require(sceLoginDialogOpen(param) == 0); Require(sceLoginDialogGetStatus() == kStatusFinished); Require(sceLoginDialogUpdateStatus() == kStatusFinished); - Require(sceLoginDialogGetResult(nullptr) == kErrArgNull); + Require(sceLoginDialogGetResult(nullptr) == kErrParamInvalid); Require(sceLoginDialogGetResult(result) == 0); - Require(result[0] == kResultUserCanceled); - Require(result[1] == -1); + RequireResult(result); Require(sceLoginDialogClose() == 0); Require(sceLoginDialogTerminate() == 0); diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 98eb2d3d10..98c40e78fc 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -153,6 +153,7 @@ long double APS5_VABI log1pl_nid_postfix(long double); long double APS5_VABI log2l_nid_postfix(long double); long double APS5_VABI lgammal_nid_postfix(long double); long double APS5_VABI tgammal_nid_postfix(long double); +float APS5_VABI tgammaf_nid_postfix(float); long double APS5_VABI hypotl_nid_postfix(long double, long double); float APS5_VABI hypot3f_nid_postfix(float, float, float); double APS5_VABI hypot3_nid_postfix(double, double, double); @@ -316,42 +317,136 @@ static bool ExtendedBits(long double value, std::uint64_t mantissa, std::uint16_ } static void CheckNanConversions() { - const struct { const char* text; std::size_t consumed; bool negative; } cases[] = { - {"nan(0x123)", 10, false}, {" -NaN(1)!", 8, true}, {"nan(0x800000)", 13, false}, - {"nan(0x10000000000000)", 21, false}, {"nan(0x8000000000000000)", 23, false}, + struct MeasuredNan { + const char* text; + std::uint64_t bits; + std::size_t used; + std::size_t sequenceUsed; + bool extendedMeasured; + bool wideDoubleMeasured; + bool wideOtherMeasured; + }; + static constexpr MeasuredNan cases[] = { + {"nan", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"NAN", 0x7ff8000000000000ull, 3, 3, false, false, false}, + {"nan()", 0x7ff8000000000000ull, 5, 5, false, false, false}, + {"nan(1)", 0x7ff8000000000001ull, 6, 6, true, true, true}, + {"nan(0x10)", 0x7ff8000000000010ull, 9, 9, false, false, false}, + {"nan(abc)", 0x7ff8000000000abcull, 8, 8, false, false, false}, + {"nan(", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"-nan", 0xfff8000000000000ull, 4, 4, true, true, true}, + {"+nan(5)", 0x7ff8000000000005ull, 7, 7, false, false, false}, + {"nan(10)", 0x7ff8000000000010ull, 7, 7, false, false, false}, + {"nan(xyz)", 0x7ff8000000000000ull, 8, 8, false, false, false}, + {"nan(_)", 0x7ff8000000000000ull, 6, 6, false, false, false}, + {"nan(12", 0x7ff8000000000012ull, 3, 3, false, false, false}, + {"-nan(5)", 0xfff8000000000005ull, 7, 7, false, false, false}, + {"nan(0x8000000000000)", 0x7ff8000000000000ull, 20, 20, false, false, false}, + {"nan(0x10000000000000)", 0x7ff8000000000001ull, 21, 21, true, true, false}, + {"nan(0x400000)", 0x7ff8000000400000ull, 13, 13, false, false, false}, + {"NaN(0X1F)", 0x7ff800000000001full, 9, 9, false, false, false}, + {"nan(0x)", 0x7ff8000000000000ull, 7, 7, false, false, false}, + {"nan(123abc)", 0x7ff8000000123abcull, 11, 11, true, true, false}, + {"nan(1x2)", 0x7ff8000000000000ull, 8, 8, true, true, false}, + {"nan(0x1g)", 0x7ff8000000000000ull, 9, 9, true, true, false}, + {"nan(g1)", 0x7ff8000000000000ull, 7, 7, true, true, false}, + {"nan(0x0x1)", 0x7ff8000000000000ull, 10, 10, true, true, false}, + {"nan(fffffffffffff)", 0x7fffffffffffffffull, 18, 18, true, true, false}, + {"nan(1fffffffffffff)", 0x7fffffffffffffffull, 19, 19, true, true, false}, + {"nan(10000000000000)", 0x7ff8000000000001ull, 19, 19, true, true, false}, + {"nan(20000000000000)", 0x7ff8000000000001ull, 19, 19, true, true, false}, + {"nan(0x20000000000000)", 0x7ff8000000000001ull, 21, 21, true, true, false}, + {"nan(ffffffffffffffff)", 0x7fffffffffffffffull, 21, 21, true, true, false}, + {"nan(10000000000000000)", 0x7ff8000000000001ull, 22, 22, true, true, false}, + {"nan(0X)", 0x7ff8000000000000ull, 7, 7, true, true, false}, + {"nan(-1)", 0x7ff8000000000000ull, 7, 3, true, true, false}, + {"nan(+1)", 0x7ff8000000000000ull, 7, 3, true, true, false}, + {"nan( 1)", 0x7ff8000000000001ull, 7, 3, true, true, false}, + {"nan(1 )", 0x7ff8000100000000ull, 7, 3, true, true, false}, + {"nan(0)", 0x7ff8000000000001ull, 6, 6, true, true, false}, + {"nan(00001)", 0x7ff8000000000001ull, 10, 10, true, true, false}, + {"nan(0x0001)", 0x7ff8000000000001ull, 11, 11, true, true, false}, + {"nan(8000000000000)", 0x7ff8000000000000ull, 18, 18, true, true, false}, + {"nan(7ffffffffffff)", 0x7fffffffffffffffull, 18, 18, true, true, false}, + {"nan(123", 0x7ff8000000000123ull, 3, 3, true, true, false}, + {"nan(1)x", 0x7ff8000000000001ull, 6, 6, true, true, false}, + {"-nan(1)", 0xfff8000000000001ull, 7, 7, true, true, false}, + {"nan(abcdefABCDEF)", 0x7ff8abcdefabcdefull, 17, 17, true, true, false}, + {"nan(0xABCDEF)", 0x7ff8000000abcdefull, 13, 13, true, true, false}, + {"nan(9)", 0x7ff8000000000009ull, 6, 6, true, true, false}, + {"nan(1_2)", 0x7ff8000000000000ull, 8, 8, true, true, false}, + {"nan(_1)", 0x7ff8000000000000ull, 7, 7, true, true, false}, + {"nan(1)(2)", 0x7ff8000000000001ull, 6, 6, true, true, false}, + {"+nan", 0x7ff8000000000000ull, 4, 4, true, true, true}, + {"NaN", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"-NAN(5)", 0xfff8000000000005ull, 7, 7, true, true, true}, + {"nan(5)", 0x7ff8000000000005ull, 6, 6, true, true, true}, + {"-nan(abc)", 0xfff8000000000abcull, 9, 9, true, true, true}, + {" -nan", 0xfff8000000000000ull, 6, 6, true, true, true}, + {"nan(12 x", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"nan(12x", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"nan(1", 0x7ff8000000000001ull, 3, 3, true, true, true}, + {"nan(a b)", 0x7ff8000a0000000bull, 8, 3, true, true, true}, + {"nan(ab)", 0x7ff80000000000abull, 7, 7, true, true, true}, + {"nanx", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"-nan()", 0xfff8000000000000ull, 6, 6, true, true, true}, + {"nan(0x8000000000001)", 0x7ff8000000000001ull, 20, 20, true, true, true}, }; bool correct = true; for (const auto& test : cases) { - const char* expectedEnd = test.text + test.consumed; - const std::uint64_t expectedDouble = test.negative ? 0xfff8000000000000ull : 0x7ff8000000000000ull; char* end = nullptr; const auto value = std::bit_cast(strtod_nid_postfix(test.text, &end)); - if (value != expectedDouble || end != expectedEnd) { + if (value != test.bits || end != test.text + test.used) { std::fprintf(stderr, "strtod(\"%s\") = 0x%016llx\n", test.text, static_cast(value)); correct = false; } - const auto ascii = std::bit_cast(atof_nid_postfix(test.text)); - if (ascii != expectedDouble) { - std::fprintf(stderr, "atof(\"%s\") = 0x%016llx\n", test.text, static_cast(ascii)); + if (std::bit_cast(atof_nid_postfix(test.text)) != test.bits) { + std::fprintf(stderr, "atof(\"%s\")\n", test.text); correct = false; } const auto single = std::bit_cast(strtof_nid_postfix(test.text, &end)); - if (single != (test.negative ? 0xffc00000u : 0x7fc00000u) || end != expectedEnd) { + if (single != 0x7fc00000u || end != test.text + test.sequenceUsed) { std::fprintf(stderr, "strtof(\"%s\") = 0x%08x\n", test.text, static_cast(single)); correct = false; } - const long double extended = strtold_nid_postfix(test.text, &end); - if (!ExtendedBits(extended, 0xc000000000000000ull, test.negative ? 0xffff : 0x7fff) || end != expectedEnd) { - std::uint64_t significand = 0; - std::uint16_t signExponent = 0; - std::memcpy(&significand, &extended, sizeof(significand)); - std::memcpy(&signExponent, reinterpret_cast(&extended) + sizeof(significand), sizeof(signExponent)); - std::fprintf(stderr, "strtold(\"%s\") = 0x%04x'%016llx\n", test.text, static_cast(signExponent), - static_cast(significand)); - correct = false; + if (test.extendedMeasured) { + const long double extended = strtold_nid_postfix(test.text, &end); + if (!ExtendedBits(extended, 0xc000000000000000ull, 0x7fff) || end != test.text + test.sequenceUsed) { + std::fprintf(stderr, "strtold(\"%s\")\n", test.text); + correct = false; + } + } + char16_t wide[64] = {}; + for (std::size_t i = 0; test.text[i] != 0; ++i) wide[i] = static_cast(test.text[i]); + char16_t* wideEnd = nullptr; + if (test.wideDoubleMeasured) { + const auto wideValue = std::bit_cast(wcstod_nid_postfix(wide, &wideEnd)); + if (wideValue != test.bits || wideEnd != wide + test.used) { + std::fprintf(stderr, "wcstod(\"%s\") = 0x%016llx\n", test.text, static_cast(wideValue)); + correct = false; + } + } + if (test.wideOtherMeasured) { + const auto wideSingle = std::bit_cast(wcstof_nid_postfix(wide, &wideEnd)); + const bool singleOk = wideSingle == 0x7fc00000u && wideEnd == wide + test.sequenceUsed; + const long double wideExtended = wcstold_nid_postfix(wide, &wideEnd); + if (!singleOk || !ExtendedBits(wideExtended, 0xc000000000000000ull, 0x7fff) || wideEnd != wide + test.sequenceUsed) { + std::fprintf(stderr, "wcstof/wcstold(\"%s\")\n", test.text); + correct = false; + } } } Require(correct); + for (const char* text : {"nan(1 ", "nan(12 \t"}) { + bool threw = false; + try { strtod_nid_postfix(text, nullptr); } catch (...) { threw = true; } + Require(threw); + } + struct PlainNan { const char* text; std::uint64_t bits; std::size_t used; }; + for (const PlainNan& plain : {PlainNan{"nan(1 2)", 0x7ff8000100000002ull, 8}, PlainNan{"nan( \0)", 0x7ff8000000000000ull, 3}}) { + char* end = nullptr; + Require(std::bit_cast(strtod_nid_postfix(plain.text, &end)) == plain.bits && end == plain.text + plain.used); + } } static void CheckHypotSpecialValue() { @@ -862,6 +957,9 @@ static void CheckLongDoubleFunctions() { Require(log2l_nid_postfix(8.0L) == 3.0L && log2l_nid_postfix(0x1p-16445L) == -16445.0L); Require(lgammal_nid_postfix(1.0L) == 0.0L && lgammal_nid_postfix(2.0L) == 0.0L && lgammal_nid_postfix(0.0L) == infinity); Require(tgammal_nid_postfix(5.0L) == 24.0L && Near(tgammal_nid_postfix(0.5L), 1.7724538509055160272981675L)); + Require(tgammaf_nid_postfix(5.0f) == 24.0f && std::abs(tgammaf_nid_postfix(0.5f) - 1.77245385f) < 1e-6f); + Require(tgammaf_nid_postfix(1.0f) == 1.0f && tgammaf_nid_postfix(2.0f) == 1.0f && tgammaf_nid_postfix(3.0f) == 2.0f); + Require(std::isinf(tgammaf_nid_postfix(0.0f)) && std::isnan(tgammaf_nid_postfix(-1.0f))); Require(hypotl_nid_postfix(3.0L, -4.0L) == 5.0L && hypotl_nid_postfix(nan, -infinity) == infinity); Require(hypot3f_nid_postfix(3.0f, -4.0f, 12.0f) == 13.0f && hypot3f_nid_postfix(0.0f, 3.0f, -4.0f) == 5.0f); diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 30057ad711..04fe239d47 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -2831,8 +2831,11 @@ static void CheckAvailableDirectMemoryIsLargestFreeBlock() { Require(start == top + 0x200000 && size == 0x400000); Require(sceKernelAvailableDirectMemorySize(top, top + window, 0x1000000, &start, &size) == 0); Require(start == top + 0x1000000 && size == static_cast(window - 0x1000000)); - Require(sceKernelAvailableDirectMemorySize(top, top + 0x200000, 0, &start, &size) == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelAvailableDirectMemorySize(top, top + 0x200000, 0, &start, &size) == SCE_KERNEL_ERROR_ENOMEM); Require(start == 0 && size == 0); + Require(sceKernelAvailableDirectMemorySize(top + window, top, 0, &start, &size) == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelAvailableDirectMemorySize(-1, top + window, 0, &start, &size) == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelAvailableDirectMemorySize(top, top + window, std::numeric_limits::max(), &start, &size) == SCE_KERNEL_ERROR_EAGAIN); Require(sceKernelAvailableDirectMemorySize(top, top + window, 0, nullptr, &size) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelReleaseDirectMemory(first, 0x200000) == 0); diff --git a/core/libs/tests/GuestMove.cpp b/core/libs/tests/GuestMove.cpp new file mode 100644 index 0000000000..db2b1bc998 --- /dev/null +++ b/core/libs/tests/GuestMove.cpp @@ -0,0 +1,144 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceMoveInit(void); +int APS5_VABI sceMoveTerm(void); +int APS5_VABI sceMoveOpen(std::int32_t userId, std::int32_t type, std::int32_t index); +int APS5_VABI sceMoveClose(std::int32_t handle); +int APS5_VABI sceMoveGetDeviceInfo(std::int32_t handle, void* info); +int APS5_VABI sceMoveGetExtensionPortInfo(std::int32_t handle, void* data); +int APS5_VABI sceMoveReadStateLatest(std::int32_t handle, void* data); +int APS5_VABI sceMoveReadStateRecent(std::int32_t handle, std::int64_t timestamp, void* data, std::int32_t* outCount); +int APS5_VABI sceMoveSetVibration(std::int32_t handle, std::uint8_t motor); +int APS5_VABI sceMoveSetLightSphere(std::int32_t handle, std::uint8_t red, std::uint8_t green, std::uint8_t blue); +int APS5_VABI sceMoveResetLightSphere(std::int32_t handle); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kOk = 0; +constexpr int kNoControllerConnected = 1; +constexpr int kNotInit = static_cast(0x80EE0001); +constexpr int kAlreadyInit = static_cast(0x80EE0002); +constexpr int kInvalidArg = static_cast(0x80EE0003); +constexpr std::int32_t kUserId = 0x10000000; +constexpr std::int32_t kFirstHandle = 0x30B0100; +constexpr std::int32_t kHandleStep = 0x100; +constexpr std::uint8_t kFill = 0x5A; + +struct Buffer { + std::uint8_t bytes[128]; + + Buffer() { std::memset(bytes, kFill, sizeof(bytes)); } + + bool Untouched() const { + for (std::uint8_t byte : bytes) { + if (byte != kFill) return false; + } + return true; + } +}; + +template +bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +void RequireNotInit(std::int32_t handle) { + Buffer buffer; + std::int32_t count = -7; + Require(sceMoveOpen(kUserId, 0, 0) == kNotInit); + Require(sceMoveClose(handle) == kNotInit); + Require(sceMoveGetDeviceInfo(handle, buffer.bytes) == kNotInit); + Require(sceMoveGetExtensionPortInfo(handle, buffer.bytes) == kNotInit); + Require(sceMoveReadStateLatest(handle, buffer.bytes) == kNotInit); + Require(sceMoveReadStateRecent(handle, 0, buffer.bytes, &count) == kNotInit); + Require(sceMoveSetVibration(handle, 0xFF) == kNotInit); + Require(sceMoveSetLightSphere(handle, 0xFF, 0, 0) == kNotInit); + Require(sceMoveResetLightSphere(handle) == kNotInit); + Require(sceMoveTerm() == kNotInit); + Require(buffer.Untouched()); + Require(count == -7); +} + +void RequireUnknownHandleThrows(std::int32_t handle) { + Buffer buffer; + std::int32_t count = -7; + Require(Throws([&] { sceMoveClose(handle); })); + Require(Throws([&] { sceMoveGetDeviceInfo(handle, buffer.bytes); })); + Require(Throws([&] { sceMoveGetExtensionPortInfo(handle, buffer.bytes); })); + Require(Throws([&] { sceMoveReadStateLatest(handle, buffer.bytes); })); + Require(Throws([&] { sceMoveReadStateRecent(handle, 0, buffer.bytes, &count); })); + Require(Throws([&] { sceMoveSetVibration(handle, 0xFF); })); + Require(Throws([&] { sceMoveSetLightSphere(handle, 0xFF, 0, 0); })); + Require(Throws([&] { sceMoveResetLightSphere(handle); })); + Require(buffer.Untouched()); + Require(count == -7); +} + +} + +int main() { + RequireNotInit(kFirstHandle); + + Require(sceMoveInit() == kOk); + Require(sceMoveInit() == kAlreadyInit); + + RequireUnknownHandleThrows(kFirstHandle); + + const std::int32_t first = sceMoveOpen(kUserId, 0, 0); + const std::int32_t second = sceMoveOpen(kUserId, 0, 1); + Require(first == kFirstHandle); + Require(second == kFirstHandle + kHandleStep); + Require(Throws([] { sceMoveOpen(kUserId, 0, 0); })); + Require(Throws([] { sceMoveOpen(kUserId + 1, 0, 2); })); + + Require(sceMoveGetDeviceInfo(first, nullptr) == kInvalidArg); + Require(sceMoveGetExtensionPortInfo(first, nullptr) == kInvalidArg); + Require(sceMoveReadStateLatest(first, nullptr) == kInvalidArg); + + Buffer buffer; + std::int32_t count = -7; + Require(sceMoveReadStateRecent(first, -1, buffer.bytes, &count) == kInvalidArg); + Require(sceMoveReadStateRecent(first, 0, nullptr, &count) == kInvalidArg); + Require(sceMoveReadStateRecent(first, 0, buffer.bytes, nullptr) == kInvalidArg); + + Require(sceMoveGetDeviceInfo(first, buffer.bytes) == kNoControllerConnected); + Require(sceMoveGetExtensionPortInfo(first, buffer.bytes) == kNoControllerConnected); + Require(sceMoveReadStateLatest(first, buffer.bytes) == kNoControllerConnected); + Require(sceMoveReadStateRecent(first, 0, buffer.bytes, &count) == kNoControllerConnected); + Require(buffer.Untouched()); + Require(count == -7); + + Require(sceMoveSetVibration(first, 0xFF) == kNoControllerConnected); + Require(sceMoveSetLightSphere(first, 0, 0xFF, 0) == kNoControllerConnected); + Require(sceMoveResetLightSphere(first) == kOk); + + Require(sceMoveClose(first) == kOk); + RequireUnknownHandleThrows(first); + const std::int32_t reopened = sceMoveOpen(kUserId, 0, 0); + Require(reopened == kFirstHandle + 2 * kHandleStep); + Require(sceMoveClose(reopened) == kOk); + Require(sceMoveSetVibration(second, 0) == kNoControllerConnected); + Require(sceMoveTerm() == kOk); + + RequireNotInit(second); + + Require(sceMoveInit() == kOk); + RequireUnknownHandleThrows(second); + const std::int32_t third = sceMoveOpen(kUserId, 0, 0); + Require(third == kFirstHandle + 3 * kHandleStep); + Require(sceMoveClose(third) == kOk); + Require(sceMoveTerm() == kOk); +} diff --git a/core/libs/tests/GuestNetCtlAp.cpp b/core/libs/tests/GuestNetCtlAp.cpp new file mode 100644 index 0000000000..fbb8b0f01d --- /dev/null +++ b/core/libs/tests/GuestNetCtlAp.cpp @@ -0,0 +1,134 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNetCtlApInit(void); +void APS5_VABI sceNetCtlApTerm(void); +int APS5_VABI sceNetCtlApCheckCallback(void); +int APS5_VABI sceNetCtlApClearEvent(int eventType); +int APS5_VABI sceNetCtlApRegisterCallback(void (APS5_VABI *func)(int, void*), void* arg, int* cid); +int APS5_VABI sceNetCtlApUnregisterCallback(int cid); +int APS5_VABI sceNetCtlApGetResult(int eventType, int* errorCode); +int APS5_VABI sceNetCtlApGetInfo(void* info); +int APS5_VABI sceNetCtlApGetState(int* state); +int APS5_VABI sceNetCtlApStop(void); +int APS5_VABI sceNetCtlApGetConnectInfo(std::uint8_t* optStr, void* connectInfo); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kErrNotStarted = static_cast(0x8041298F); +constexpr int kErrInvalidType = static_cast(0x80412990); +constexpr int kErrInvalidAddr = static_cast(0x80412991); +constexpr int kErrNotInitialized = static_cast(0x80412994); +constexpr int kErrCallbackMax = static_cast(0x80412995); +constexpr int kErrInvalidId = static_cast(0x80412996); +constexpr int kErrIdNotFound = static_cast(0x80412997); +constexpr int kErrInvalidSize = static_cast(0x80412998); + +int g_calls = 0; + +void APS5_VABI Callback(int, void*) { ++g_calls; } + +void RequireUninitialized() { + int value = 0x5a5a; + std::uint8_t block[184] = {}; + Require(sceNetCtlApCheckCallback() == kErrNotInitialized); + Require(sceNetCtlApClearEvent(1) == kErrNotInitialized); + Require(sceNetCtlApRegisterCallback(Callback, nullptr, &value) == kErrNotInitialized && value == 0x5a5a); + Require(sceNetCtlApUnregisterCallback(0) == kErrNotInitialized); + Require(sceNetCtlApGetResult(1, &value) == kErrNotInitialized && value == 0x5a5a); + Require(sceNetCtlApGetInfo(block) == kErrNotInitialized); + Require(sceNetCtlApGetState(&value) == kErrNotInitialized && value == 0x5a5a); + Require(sceNetCtlApStop() == kErrNotInitialized); + Require(sceNetCtlApGetConnectInfo(nullptr, block) == kErrNotInitialized); +} + +} + +int main() { + RequireUninitialized(); + Require(sceNetCtlApInit() == 0); + + int state = -1; + Require(sceNetCtlApGetState(&state) == 0 && state == 0); + Require(sceNetCtlApGetState(nullptr) == kErrInvalidAddr); + + std::uint8_t info[152]; + std::memset(info, 0x5a, sizeof(info)); + const std::uint64_t infoSize = sizeof(info); + std::memcpy(info, &infoSize, sizeof(infoSize)); + Require(sceNetCtlApGetInfo(info) == kErrNotStarted); + for (std::size_t i = sizeof(infoSize); i < sizeof(info); ++i) Require(info[i] == 0x5a); + Require(sceNetCtlApGetInfo(nullptr) == kErrInvalidAddr); + const std::uint64_t wrongInfoSize = sizeof(info) - 8; + std::memcpy(info, &wrongInfoSize, sizeof(wrongInfoSize)); + Require(sceNetCtlApGetInfo(info) == kErrInvalidSize); + + std::uint8_t connect[184]; + std::memset(connect, 0x5a, sizeof(connect)); + const std::uint64_t connectSize = sizeof(connect); + std::memcpy(connect, &connectSize, sizeof(connectSize)); + std::uint8_t option[] = "opt"; + Require(sceNetCtlApGetConnectInfo(option, connect) == kErrNotStarted); + Require(sceNetCtlApGetConnectInfo(nullptr, connect) == kErrNotStarted); + for (std::size_t i = sizeof(connectSize); i < sizeof(connect); ++i) Require(connect[i] == 0x5a); + Require(sceNetCtlApGetConnectInfo(option, nullptr) == kErrInvalidAddr); + const std::uint64_t wrongConnectSize = sizeof(info); + std::memcpy(connect, &wrongConnectSize, sizeof(wrongConnectSize)); + Require(sceNetCtlApGetConnectInfo(option, connect) == kErrInvalidSize); + + Require(sceNetCtlApStop() == kErrNotStarted); + + for (int type = 1; type <= 4; ++type) { + int code = -1; + Require(sceNetCtlApClearEvent(type) == 0); + Require(sceNetCtlApGetResult(type, &code) == 0 && code == 0); + Require(sceNetCtlApGetResult(type, nullptr) == kErrInvalidAddr); + } + for (const int type : {0, 5, -1}) { + int code = 0x5a5a; + Require(sceNetCtlApClearEvent(type) == kErrInvalidType); + Require(sceNetCtlApGetResult(type, &code) == kErrInvalidType && code == 0x5a5a); + } + + int cids[8]; + for (int i = 0; i < 8; ++i) { + cids[i] = -1; + Require(sceNetCtlApRegisterCallback(Callback, &cids[i], &cids[i]) == 0 && cids[i] == i); + } + int extra = -1; + Require(sceNetCtlApRegisterCallback(Callback, nullptr, &extra) == kErrCallbackMax && extra == -1); + Require(sceNetCtlApRegisterCallback(nullptr, nullptr, &extra) == kErrInvalidAddr); + Require(sceNetCtlApRegisterCallback(Callback, nullptr, nullptr) == kErrInvalidAddr); + Require(sceNetCtlApCheckCallback() == 0 && sceNetCtlApCheckCallback() == 0); + Require(g_calls == 0); + Require(sceNetCtlApUnregisterCallback(3) == 0); + Require(sceNetCtlApUnregisterCallback(3) == kErrIdNotFound); + Require(sceNetCtlApUnregisterCallback(-1) == kErrInvalidId); + Require(sceNetCtlApUnregisterCallback(8) == kErrInvalidId); + Require(sceNetCtlApRegisterCallback(Callback, nullptr, &extra) == 0 && extra == 3); + + bool threw = false; + try { + sceNetCtlApInit(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + + sceNetCtlApTerm(); + RequireUninitialized(); + Require(sceNetCtlApInit() == 0); + int cid = -1; + Require(sceNetCtlApRegisterCallback(Callback, nullptr, &cid) == 0 && cid == 0); + Require(sceNetCtlApGetState(&state) == 0 && state == 0); + sceNetCtlApTerm(); + sceNetCtlApTerm(); + RequireUninitialized(); +} diff --git a/core/libs/tests/GuestNpAuth.cpp b/core/libs/tests/GuestNpAuth.cpp index 92cdf6c335..e9352d3f8c 100644 --- a/core/libs/tests/GuestNpAuth.cpp +++ b/core/libs/tests/GuestNpAuth.cpp @@ -8,7 +8,10 @@ int APS5_VABI sceNpAuthAbortRequest(int req_id); int APS5_VABI sceNpAuthCreateAsyncRequest(const void* param); int APS5_VABI sceNpAuthCreateRequest(void); int APS5_VABI sceNpAuthDeleteRequest(int req_id); +int APS5_VABI sceNpAuthGetAuthorizationCode(int req_id, const void* param, void* auth_code); int APS5_VABI sceNpAuthGetAuthorizationCodeV3(int req_id, const void* param, void* auth_code, int* issuer_id); +int APS5_VABI sceNpAuthGetAuthorizedAppCode(); +int APS5_VABI sceNpAuthGetIdToken(int req_id, const void* param, void* id_token); int APS5_VABI sceNpAuthGetIdTokenV3(int req_id, const void* param, void* id_token); int APS5_VABI sceNpAuthPollAsync(int req_id, int* result); int APS5_VABI sceNpAuthSetTimeout(int req_id, int resolve_retry, uint32_t resolve_timeout, uint32_t connect_timeout, uint32_t send_timeout, uint32_t receive_timeout); @@ -52,14 +55,24 @@ int main() { int waitUnprocessed = 0; Require(sceNpAuthWaitAsync(asyncReq, &waitUnprocessed) == invalidId); + Require(sceNpAuthGetAuthorizedAppCode() == signedOut); + + Require(sceNpAuthGetIdToken(syncReq, nullptr, token) == invalidArgument); + Require(sceNpAuthGetIdToken(syncReq, param, nullptr) == invalidArgument); + Require(sceNpAuthGetIdToken(syncReq, nullptr, nullptr) == invalidArgument); Require(sceNpAuthGetIdTokenV3(syncReq, nullptr, token) == invalidArgument); Require(sceNpAuthGetIdTokenV3(syncReq, param, nullptr) == invalidArgument); Require(sceNpAuthGetIdTokenV3(syncReq, nullptr, nullptr) == invalidArgument); - Require(sceNpAuthGetIdTokenV3(syncReq, param, token) == signedOut); + Require(sceNpAuthGetIdToken(syncReq, param, token) == signedOut); + Require(sceNpAuthGetIdToken(syncReq, param, token) == invalidArgument); Require(sceNpAuthGetIdTokenV3(syncReq, param, token) == invalidArgument); + + Require(sceNpAuthGetAuthorizationCode(syncReq2, nullptr, authCode) == invalidArgument); + Require(sceNpAuthGetAuthorizationCode(syncReq2, param, nullptr) == invalidArgument); Require(sceNpAuthGetAuthorizationCodeV3(syncReq, nullptr, authCode, &issuerId) == invalidArgument); Require(sceNpAuthGetAuthorizationCodeV3(syncReq, param, nullptr, &issuerId) == invalidArgument); - Require(sceNpAuthGetAuthorizationCodeV3(syncReq2, param, authCode, &issuerId) == signedOut); + Require(sceNpAuthGetAuthorizationCode(syncReq2, param, authCode) == signedOut); + Require(sceNpAuthGetAuthorizationCode(syncReq2, param, authCode) == invalidArgument); Require(sceNpAuthGetAuthorizationCodeV3(syncReq2, param, authCode, &issuerId) == invalidArgument); Require(sceNpAuthGetIdTokenV3(asyncReq, param, token) == 0); @@ -82,7 +95,9 @@ int main() { Require(asyncWait2 == signedOut); constexpr int unknownReq = 999999; + Require(sceNpAuthGetIdToken(unknownReq, param, token) == requestNotFound); Require(sceNpAuthGetIdTokenV3(unknownReq, param, token) == requestNotFound); + Require(sceNpAuthGetAuthorizationCode(unknownReq, param, authCode) == requestNotFound); Require(sceNpAuthGetAuthorizationCodeV3(unknownReq, param, authCode, &issuerId) == requestNotFound); int pollResult = 0; Require(sceNpAuthPollAsync(unknownReq, &pollResult) == requestNotFound); diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index e17b0521d1..6e79af4b49 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -243,6 +243,10 @@ static void CheckReadStateHandle(int handle) { Require(scePadReadState(handle + 1, &data) == PAD_ERROR_INVALID_HANDLE); Require(scePadRead_nid_postfix(0, &data, 1) == PAD_ERROR_INVALID_HANDLE); Require(scePadReadState(handle, &data) == PAD_OK); + Require(scePadRead_nid_postfix(handle, nullptr, 1) == PAD_ERROR_INVALID_ARG); + Require(scePadRead_nid_postfix(handle, &data, 0) == PAD_ERROR_INVALID_ARG); + Require(scePadRead_nid_postfix(handle, &data, -1) == PAD_ERROR_INVALID_ARG); + Require(scePadReadState(handle, nullptr) == PAD_ERROR_INVALID_ARG); } static void CheckRemoteController(int handle) { diff --git a/core/libs/tests/GuestPathNameLength.cpp b/core/libs/tests/GuestPathNameLength.cpp new file mode 100644 index 0000000000..e07722661b --- /dev/null +++ b/core/libs/tests/GuestPathNameLength.cpp @@ -0,0 +1,74 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI mkdir_nid_postfix(const char*, int); +int APS5_VABI rmdir_nid_postfix(const char*); +int APS5_VABI unlink_nid_postfix(const char*); +int APS5_VABI rename_nid_postfix(const char*, const char*); +int APS5_VABI stat_nid_postfix(const char*, void*); +int APS5_VABI access_nid_postfix(const char*, int); +int APS5_VABI chflags_nid_postfix(const char*, std::uint64_t); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int ErrorNoEntry = 2; +constexpr int ErrorNameTooLong = 63; + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + std::abort(); +} + +bool Fails(long result, int error) { return result == -1 && *__error_nid_postfix() == error; } + +bool TooLong(const std::string& path) { + unsigned char status[256]; + return Fails(stat_nid_postfix(path.c_str(), status), ErrorNameTooLong); +} + +} + +int main() { + unsigned char status[256]; + const std::string longName(256, 'n'); + const std::string longPath(1100, 'p'); + const std::string maxName(255, 'm'); + Require(Fails(stat_nid_postfix(longName.c_str(), status), ErrorNameTooLong), "stat of a name above NAME_MAX (255) must fail with ENAMETOOLONG (63)"); + rmdir_nid_postfix("anyps5_name_length"); + Require(mkdir_nid_postfix("anyps5_name_length", 0755) == 0, "mkdir of the test directory failed"); + Require(Fails(stat_nid_postfix(("anyps5_name_length/" + longName).c_str(), status), ErrorNameTooLong), "stat of a long final component below an existing directory must fail with ENAMETOOLONG (63)"); + Require(Fails(stat_nid_postfix(("anyps5_name_length/missing/" + longName).c_str(), status), ErrorNoEntry), "stat of a long component below a missing directory must fail with ENOENT (2), the lookup stops there first"); + const std::string below = "anyps5_name_length/"; + const std::string name255(255, 'n'); + Require(!TooLong(below + name255), "a final component of exactly 255 bytes must not be too long"); + Require(!TooLong(below + name255 + "/x"), "a middle component of exactly 255 bytes must not be too long"); + Require(TooLong(below + longName + "/x"), "a middle component of 256 bytes must fail with ENAMETOOLONG (63)"); + Require(TooLong(below + longName + "/"), "a long component followed by a trailing slash must fail with ENAMETOOLONG (63)"); + Require(!TooLong(below + std::string(200, 'n') + "\\" + std::string(100, 'n')), "a backslash separates components here, so 200 + 100 bytes around it are accepted (FreeBSD would refuse the 301-byte name)"); + const std::string block = name255 + "/"; + Require(!TooLong(block + block + block + name255), "a path of exactly 1023 bytes must not be too long"); + Require(TooLong(block + block + block + block), "a path of exactly 1024 bytes must fail with ENAMETOOLONG (63)"); + Require(rmdir_nid_postfix("anyps5_name_length") == 0, "rmdir of the test directory failed"); + Require(Fails(stat_nid_postfix((longName + "/x").c_str(), status), ErrorNameTooLong), "stat of a long first component must fail with ENAMETOOLONG (63)"); + Require(Fails(stat_nid_postfix(longPath.c_str(), status), ErrorNameTooLong), "stat of a path of 1100 bytes must fail with ENAMETOOLONG (63)"); + Require(Fails(access_nid_postfix(longName.c_str(), 0), ErrorNameTooLong), "access of a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(chflags_nid_postfix(longName.c_str(), 0), ErrorNameTooLong), "chflags of a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(unlink_nid_postfix(longName.c_str()), ErrorNameTooLong), "unlink of a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(mkdir_nid_postfix(longName.c_str(), 0755), ErrorNameTooLong), "mkdir of a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(rmdir_nid_postfix(longName.c_str()), ErrorNameTooLong), "rmdir of a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(open_nid_postfix(longName.c_str(), 0, 0), ErrorNameTooLong), "open of a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(rename_nid_postfix(longName.c_str(), "anyps5_name_length_target"), ErrorNameTooLong), "rename from a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(rename_nid_postfix("anyps5_name_length_source", longName.c_str()), ErrorNameTooLong), "rename to a long name must fail with ENAMETOOLONG (63)"); + + Require(Fails(stat_nid_postfix(maxName.c_str(), status), ErrorNoEntry), "a name of exactly 255 bytes is not too long"); + return 0; +} diff --git a/core/libs/tests/GuestPlayGo.cpp b/core/libs/tests/GuestPlayGo.cpp index 0d24636d4b..4dd0d6a5f7 100644 --- a/core/libs/tests/GuestPlayGo.cpp +++ b/core/libs/tests/GuestPlayGo.cpp @@ -10,6 +10,7 @@ #include #include #include +#include extern "C" { int APS5_VABI scePlayGoInitialize(const PlayGoInitParams*); @@ -71,7 +72,15 @@ static int CheckScenarioWithoutChunkTable(const char* root) { const PlayGoToDo todo[2] = {{0, 3, 0}, {0xFFFF, 3, 0}}; Require(scePlayGoSetToDoList(handle, todo, 2) == 0); std::uint32_t chunkCount = 0; - Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1); + Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1000); + std::vector ids(chunkCount + 1, 0xFFFF); + Require(scePlayGoGetChunkId(handle, ids.data(), static_cast(ids.size()), &chunkCount) == 0 && chunkCount == 1000); + for (std::uint32_t index = 0; index < chunkCount; ++index) Require(ids[index] == index); + std::vector listedLoci(chunkCount, 0); + Require(scePlayGoGetLocus(handle, ids.data(), chunkCount, listedLoci.data()) == 0); + for (const auto locus : listedLoci) Require(locus == 3); + std::uint16_t firstTwo[2] = {0xFFFF, 0xFFFF}; + Require(scePlayGoGetChunkId(handle, firstTwo, 2, &chunkCount) == 0 && chunkCount == 2 && firstTwo[0] == 0 && firstTwo[1] == 1); Require(scePlayGoClose(handle) == 0); return 0; } diff --git a/core/libs/tests/GuestPthreadAttr.cpp b/core/libs/tests/GuestPthreadAttr.cpp index 0b28513f9d..5afa252aa8 100644 --- a/core/libs/tests/GuestPthreadAttr.cpp +++ b/core/libs/tests/GuestPthreadAttr.cpp @@ -1,7 +1,9 @@ #include "SceTypes.hpp" +#include #include #include #include +#include extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); @@ -30,6 +32,11 @@ int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr) int APS5_VABI pthread_attr_setschedparam_nid_postfix(PthreadAttr* attr, const KernelSchedParam* param); int APS5_VABI pthread_attr_setschedpolicy_nid_postfix(PthreadAttr* attr, int policy); int APS5_VABI pthread_attr_getschedpolicy_nid_postfix(const PthreadAttr* attr, int* policy); +int APS5_VABI scePthreadAttrSetdetachstate(PthreadAttr* attr, int state); +int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t destructor); +int APS5_VABI scePthreadKeyDelete(PthreadKey key); +int APS5_VABI scePthreadSetspecific(PthreadKey key, void* value); +Pthread APS5_VABI scePthreadSelf(); } static constexpr int SCE_OK = 0; @@ -47,6 +54,9 @@ static constexpr std::size_t STACK_SIZE = 2u << 20; static constexpr std::size_t MIN_STACK_SIZE = 16384; static constexpr int GUEST_EINVAL = 22; static constexpr int GUEST_ENOTSUP = 45; +static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003); +static constexpr int DETACH_DETACHED = 1; +static constexpr int DETACHED_THREADS = 32; static void Require(bool value) { if (!value) std::abort(); } @@ -256,12 +266,86 @@ static void CheckThreadScope() { Require(scePthreadJoin(inheritedThread, nullptr) == SCE_OK); } +static int QueryResult(Pthread thread) { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(scePthreadAttrSetstacksize(&attr, STACK_SIZE) == SCE_OK); + const int result = scePthreadAttrGet(thread, &attr); + std::size_t stackSize = 0; + Require(scePthreadAttrGetstacksize(&attr, &stackSize) == SCE_OK); + Require(result == SCE_OK || stackSize == STACK_SIZE); + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + return result; +} + +static bool ReportedEnded(Pthread thread) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (QueryResult(thread) == SCE_OK) { + if (std::chrono::steady_clock::now() > deadline) return false; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return QueryResult(thread) == SCE_KERNEL_ERROR_ESRCH; +} + +struct ExitQuery { + PthreadKey key = -1; + int result = -1; +}; + +static void APS5_VABI QuerySelfAtExit(void* value) { + auto* query = static_cast(value); + query->result = QueryResult(scePthreadSelf()); +} + +static void* APS5_VABI SetExitQuery(void* arg) { + auto* query = static_cast(arg); + Require(scePthreadSetspecific(query->key, query) == SCE_OK); + return nullptr; +} + +static void* APS5_VABI Finish(void*) { + return nullptr; +} + +static void CheckEndedThreads() { + Require(QueryResult(scePthreadSelf()) == SCE_OK); + + Pthread joined = nullptr; + Require(scePthreadCreate(&joined, nullptr, Finish, nullptr, nullptr) == SCE_OK); + Require(scePthreadJoin(joined, nullptr) == SCE_OK); + Require(QueryResult(joined) == SCE_KERNEL_ERROR_ESRCH); + PthreadAttr unset = nullptr; + Require(scePthreadAttrGet(joined, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGet(joined, &unset) == SCE_KERNEL_ERROR_EINVAL); + + ExitQuery exitQuery; + Require(scePthreadKeyCreate(&exitQuery.key, reinterpret_cast(QuerySelfAtExit)) == SCE_OK); + Pthread finished = nullptr; + Require(scePthreadCreate(&finished, nullptr, SetExitQuery, &exitQuery, nullptr) == SCE_OK); + Require(ReportedEnded(finished)); + Require(scePthreadJoin(finished, nullptr) == SCE_OK); + Require(exitQuery.result == SCE_OK); + Require(QueryResult(finished) == SCE_KERNEL_ERROR_ESRCH); + Require(scePthreadKeyDelete(exitQuery.key) == SCE_OK); + + PthreadAttr detachedAttr = nullptr; + Require(scePthreadAttrInit(&detachedAttr) == SCE_OK); + Require(scePthreadAttrSetdetachstate(&detachedAttr, DETACH_DETACHED) == SCE_OK); + for (int index = 0; index < DETACHED_THREADS; ++index) { + Pthread detached = nullptr; + Require(scePthreadCreate(&detached, &detachedAttr, Finish, nullptr, nullptr) == SCE_OK); + Require(ReportedEnded(detached)); + } + Require(scePthreadAttrDestroy(&detachedAttr) == SCE_OK); +} + int main() { CheckStackSizeLimit(); CheckSoloSched(); CheckSchedArguments(); CheckSchedulingAttributes(); CheckThreadScope(); + CheckEndedThreads(); PthreadAttr attr = nullptr; Require(scePthreadAttrInit(&attr) == SCE_OK); Require(scePthreadAttrSetinheritsched(&attr, EXPLICIT_SCHED) == SCE_OK); diff --git a/core/libs/tests/GuestPthreadMutexattr.cpp b/core/libs/tests/GuestPthreadMutexattr.cpp index 7e704677e5..48dd291a5b 100644 --- a/core/libs/tests/GuestPthreadMutexattr.cpp +++ b/core/libs/tests/GuestPthreadMutexattr.cpp @@ -10,6 +10,7 @@ int APS5_VABI pthread_mutexattr_destroy_nid_postfix(PthreadMutexattr* attr); int APS5_VABI pthread_mutexattr_getpshared_nid_postfix(const PthreadMutexattr* attr, int* pshared); int APS5_VABI pthread_mutexattr_setpshared_nid_postfix(PthreadMutexattr* attr, int pshared); int APS5_VABI pthread_mutexattr_setprotocol_nid_postfix(PthreadMutexattr* attr, int protocol); +int APS5_VABI pthread_mutexattr_getprotocol_nid_postfix(const PthreadMutexattr* attr, int* protocol); int APS5_VABI pthread_mutexattr_setprioceiling_nid_postfix(PthreadMutexattr* attr, int prioceiling); int APS5_VABI pthread_mutexattr_getprioceiling_nid_postfix(const PthreadMutexattr* attr, int* prioceiling); int APS5_VABI pthread_mutexattr_settype_nid_postfix(PthreadMutexattr* attr, int type); @@ -114,8 +115,11 @@ static void CheckProcessSharing() { static void CheckInvalidPriorityAttribute(PthreadMutexattr* attr) { int ceiling = -1; + int protocol = -1; Require(pthread_mutexattr_setprotocol_nid_postfix(attr, PRIO_PROTECT) == GUEST_EINVAL); Require(pthread_mutexattr_setprotocol_nid_postfix(attr, -1) == GUEST_EINVAL); + Require(pthread_mutexattr_getprotocol_nid_postfix(attr, &protocol) == GUEST_EINVAL && protocol == -1); + Require(pthread_mutexattr_getprotocol_nid_postfix(attr, nullptr) == GUEST_EINVAL); Require(pthread_mutexattr_setprioceiling_nid_postfix(attr, 300) == GUEST_EINVAL); Require(pthread_mutexattr_getprioceiling_nid_postfix(attr, &ceiling) == GUEST_EINVAL && ceiling == -1); Require(pthread_mutexattr_getprioceiling_nid_postfix(attr, nullptr) == GUEST_EINVAL); @@ -130,11 +134,15 @@ int main() { CheckInvalidPriorityAttribute(&attr); Require(pthread_mutexattr_init_nid_postfix(&attr) == 0); int ceiling = -1; + int protocol = -1; + Require(pthread_mutexattr_getprotocol_nid_postfix(&attr, &protocol) == 0 && protocol == PRIO_NONE); Require(pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling) == GUEST_EINVAL && ceiling == -1); Require(pthread_mutexattr_setprioceiling_nid_postfix(&attr, 300) == GUEST_EINVAL); Require(pthread_mutexattr_setprotocol_nid_postfix(&attr, PRIO_INHERIT) == 0); + Require(pthread_mutexattr_getprotocol_nid_postfix(&attr, &protocol) == 0 && protocol == PRIO_INHERIT); Require(pthread_mutexattr_setprioceiling_nid_postfix(&attr, 300) == GUEST_EINVAL); Require(pthread_mutexattr_setprotocol_nid_postfix(&attr, PRIO_PROTECT) == 0); + Require(pthread_mutexattr_getprotocol_nid_postfix(&attr, &protocol) == 0 && protocol == PRIO_PROTECT); Require(pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling) == 0 && ceiling == sched_get_priority_max_nid_postfix(SCHED_RR)); Require(pthread_mutexattr_setprioceiling_nid_postfix(&attr, 300) == 0); Require(pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling) == 0 && ceiling == 300); @@ -144,9 +152,12 @@ int main() { Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); Require(pthread_mutex_destroy_nid_postfix(&mutex) == 0); Require(pthread_mutexattr_setprotocol_nid_postfix(&attr, PRIO_PROTECT) == 0); + Require(pthread_mutexattr_getprotocol_nid_postfix(&attr, &protocol) == 0 && protocol == PRIO_PROTECT); Require(pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling) == 0 && ceiling == sched_get_priority_max_nid_postfix(SCHED_RR)); Require(pthread_mutexattr_setprotocol_nid_postfix(&attr, PRIO_NONE) == 0); + Require(pthread_mutexattr_getprotocol_nid_postfix(&attr, &protocol) == 0 && protocol == PRIO_NONE); Require(pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling) == GUEST_EINVAL); + Require(Throws([&] { pthread_mutexattr_getprotocol_nid_postfix(&attr, nullptr); })); Require(Throws([&] { pthread_mutexattr_getprioceiling_nid_postfix(&attr, nullptr); })); Require(pthread_mutexattr_destroy_nid_postfix(&attr) == 0); CheckInvalidPriorityAttribute(&attr); diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp index 1f882c248d..fd74e36771 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -41,7 +41,7 @@ static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003); static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003c); static constexpr int SIGUSR1 = 30; -static constexpr int Repeats = 100; +static constexpr int Repeats = 50; static constexpr std::size_t R12Offset = 0xa0; static constexpr std::size_t R13Offset = 0xa8; static constexpr std::size_t Xmm8Offset = 0x140 + 160 + 8 * 16; @@ -172,7 +172,7 @@ static void* APS5_VABI HostBlocked(void* arg) { return nullptr; } -static constexpr int LeavingRounds = 200; +static constexpr int LeavingRounds = 100; static std::atomic leavingRound{0}; static void* APS5_VABI Leaving(void* arg) { @@ -198,7 +198,7 @@ static LONG CALLBACK ContinueRaised(EXCEPTION_POINTERS* info) { return EXCEPTION_CONTINUE_EXECUTION; } -static constexpr int SelfingRounds = 100; +static constexpr int SelfingRounds = 50; static std::atomic selfSink{nullptr}; static void* APS5_VABI Selfing(void* arg) { @@ -239,7 +239,7 @@ static void* APS5_VABI Looping(void* arg) { return nullptr; } -static constexpr int VectorRounds = 100; +static constexpr int VectorRounds = 50; static constexpr std::size_t VectorBytes = 16 * 32; alignas(32) static std::uint8_t vectorPattern[VectorBytes]; alignas(32) static std::uint8_t vectorResult[VectorBytes]; @@ -405,7 +405,7 @@ static void NestedWaitKeepsOuterWake() { } #ifdef _WIN32 -static constexpr int AddressRounds = 100; +static constexpr int AddressRounds = 50; static std::uint32_t addressWord = 0; static std::uint32_t otherAddressWord = 0; static std::atomic holdingEntered{false}; diff --git a/core/libs/tests/GuestRaiseInHostCode.cpp b/core/libs/tests/GuestRaiseInHostCode.cpp new file mode 100644 index 0000000000..955d4605b6 --- /dev/null +++ b/core/libs/tests/GuestRaiseInHostCode.cpp @@ -0,0 +1,69 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread*, const PthreadAttr*, PthreadEntry, void*, const char*); +int APS5_VABI scePthreadJoin(Pthread, void**); +int APS5_VABI sceKernelInstallExceptionHandler(int, void*); +int APS5_VABI sceKernelRemoveExceptionHandler(int); +int APS5_VABI sceKernelRaiseException(Pthread, int); +} + +static constexpr int SigUsr1 = 30; +static constexpr int Raises = 100; +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 started{false}; + +static void APS5_VABI Handler(int, void*) { + const auto delivery = deliveries.fetch_add(1) + 1; + while (released.load() < delivery) std::this_thread::yield(); +} + +static void* APS5_VABI Writer(void*) { + const std::string block(1u << 20u, 'x'); + started.store(true); + while (!stop.load()) { + Require(std::fwrite(block.data(), 1, block.size(), shared) == block.size()); + Require(std::fseek(shared, 0, SEEK_SET) == 0); + volatile std::uint64_t spin = 0; + for (std::uint64_t i = 0; i < 200000; ++i) spin = spin + i; + } + return nullptr; +} + +int main() { + std::atomic done{false}; + std::thread watchdog([&done] { + for (int i = 0; i < 20000 && !done.load(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (!done.load()) std::abort(); + }); + shared = std::tmpfile(); + Require(shared != nullptr); + Require(sceKernelInstallExceptionHandler(SigUsr1, reinterpret_cast(&Handler)) == 0); + Pthread writer = nullptr; + Require(scePthreadCreate(&writer, nullptr, Writer, nullptr, "host writer") == 0); + while (!started.load()) std::this_thread::yield(); + for (int raise = 1; raise <= Raises; ++raise) { + 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); + Require(scePthreadJoin(writer, nullptr) == 0); + Require(sceKernelRemoveExceptionHandler(SigUsr1) == 0); + std::fclose(shared); + done.store(true); + watchdog.join(); +} diff --git a/core/libs/tests/GuestRenameDot.cpp b/core/libs/tests/GuestRenameDot.cpp new file mode 100644 index 0000000000..ccebfad2e9 --- /dev/null +++ b/core/libs/tests/GuestRenameDot.cpp @@ -0,0 +1,80 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI mkdir_nid_postfix(const char*, int); +int APS5_VABI rmdir_nid_postfix(const char*); +int APS5_VABI rename_nid_postfix(const char*, const char*); +int APS5_VABI sceKernelRename(const char*, const char*); +int APS5_VABI stat_nid_postfix(const char*, void*); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int ErrorNoEntry = 2; +constexpr int ErrorInvalid = 22; +constexpr char Source[] = "anyps5_rename_dot_source"; +constexpr char Target[] = "anyps5_rename_dot_target"; +constexpr char Moved[] = "anyps5_rename_dot_moved"; + +void Cleanup() { + rmdir_nid_postfix(Source); + rmdir_nid_postfix(Target); + rmdir_nid_postfix(Moved); +} + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + Cleanup(); + std::abort(); +} + +bool Fails(long result, int error) { return result == -1 && *__error_nid_postfix() == error; } + +bool Exists(const char* path) { + unsigned char status[256]; + return stat_nid_postfix(path, status) == 0; +} + +} + +int main() { + Cleanup(); + Require(mkdir_nid_postfix(Source, 0755) == 0 && mkdir_nid_postfix(Target, 0755) == 0, "mkdir failed"); + const std::string dotted = std::string(Source) + "/."; + const std::string dottedSlash = std::string(Source) + "/./"; + const std::string intoTarget = std::string(Target) + "/."; + Require(Fails(rename_nid_postfix(dotted.c_str(), Moved), ErrorInvalid), "rename from dir/. must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix(dottedSlash.c_str(), Moved), ErrorInvalid), "rename from dir/./ must fail with EINVAL (22)"); + Require(Exists(Source) && !Exists(Moved), "a failed rename from dir/. must leave the directory in place"); + Require(Fails(rename_nid_postfix(Source, intoTarget.c_str()), ErrorInvalid), "rename to dir/. must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix(".", Moved), ErrorInvalid), "rename of . must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix(Source, "."), ErrorInvalid), "rename onto . must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix("anyps5_rename_dot_missing/.", Moved), ErrorNoEntry), "rename from missing/. must fail with ENOENT (2)"); + const std::string dotDot = std::string(Source) + "/.."; + const std::string dotDotSlash = std::string(Source) + "/../"; + Require(Fails(rename_nid_postfix(dotDot.c_str(), Moved), ErrorInvalid), "rename from dir/.. must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix(dotDotSlash.c_str(), Moved), ErrorInvalid), "rename from dir/../ must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix(Source, (std::string(Target) + "/..").c_str()), ErrorInvalid), "rename to dir/.. must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix("..", Moved), ErrorInvalid), "rename of .. must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix("anyps5_rename_dot_missing/..", Moved), ErrorNoEntry), "rename from missing/.. must fail with ENOENT (2)"); + const std::string backslashDot = std::string(Source) + "\\."; + const std::string backslashDotDot = std::string(Source) + "\\.."; + Require(Fails(rename_nid_postfix(backslashDot.c_str(), Moved), ErrorInvalid), "rename from dir\\. must fail with EINVAL (22), the guest resolver turns the backslash into a separator"); + Require(Fails(rename_nid_postfix(backslashDotDot.c_str(), Moved), ErrorInvalid), "rename from dir\\.. must fail with EINVAL (22)"); + Require(Exists(Source) && !Exists(Moved), "failed renames from dir\\. and dir\\.. must leave the directory in place"); + Require(sceKernelRename(dotDot.c_str(), Moved) == static_cast(0x80020016u), "sceKernelRename from dir/.. must return EINVAL"); + Require(sceKernelRename(dotted.c_str(), Moved) == static_cast(0x80020016u), "sceKernelRename from dir/. must return EINVAL"); + Require(sceKernelRename(Source, intoTarget.c_str()) == static_cast(0x80020016u), "sceKernelRename to dir/. must return EINVAL"); + Require(Exists(Source) && Exists(Target) && !Exists(Moved), "failed renames must change nothing"); + + Require(rename_nid_postfix(Source, Moved) == 0 && Exists(Moved) && !Exists(Source), "an ordinary rename must still work"); + Require(sceKernelRename(Moved, Source) == 0 && Exists(Source) && !Exists(Moved), "an ordinary sceKernelRename must still work"); + Cleanup(); + return 0; +} diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index a1d820e72d..635a91a4dc 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -17,6 +17,7 @@ int APS5_VABI sceRtcGetTickResolution(void); int APS5_VABI sceRtcGetTick(const RtcDateTime*, RtcTick*); int APS5_VABI sceRtcSetTick(RtcDateTime*, const RtcTick*); int APS5_VABI sceRtcGetCurrentTick(RtcTick*); +int APS5_VABI sceRtcGetCurrentNetworkTick(RtcTick*); int APS5_VABI sceRtcGetCurrentClock(RtcDateTime*, int); int APS5_VABI sceRtcGetCurrentClockLocalTime(RtcDateTime*); int APS5_VABI sceRtcConvertUtcToLocalTime(const RtcTick*, RtcTick*); @@ -60,7 +61,12 @@ int APS5_VABI sceRtcFormatRFC3339Precise(char*, const RtcTick*, int, int); int APS5_VABI sceRtcFormatRFC3339PreciseLocalTime(char*, const RtcTick*, int); int APS5_VABI sceRtcCompareTick(const RtcTick*, const RtcTick*); int APS5_VABI sceRtcGetCurrentAdNetworkTick(RtcTick*); +int APS5_VABI sceRtcGetCurrentRawNetworkTick(RtcTick*); +int APS5_VABI sceRtcGetCurrentDebugNetworkTick(RtcTick*); int APS5_VABI sceRtcSetCurrentTick(const RtcTick*); +int APS5_VABI sceRtcSetCurrentNetworkTick(const RtcTick*); +int APS5_VABI sceRtcSetCurrentAdNetworkTick(const RtcTick*); +int APS5_VABI sceRtcSetCurrentDebugNetworkTick(const RtcTick*); } static void Require(bool value) { if (!value) std::abort(); } @@ -80,6 +86,31 @@ static bool Equal(const RtcDateTime& left, const RtcDateTime& right) { && left.minute == right.minute && left.second == right.second && left.microsecond == right.microsecond; } +enum class Add { Ticks, Microseconds, Seconds, Minutes, Hours, Days, Weeks, Months, Years }; + +struct AddCase { + Add function; + std::uint64_t source; + std::int64_t amount; + bool written; + std::uint64_t result; +}; + +static int CallAdd(Add function, RtcTick* destination, const RtcTick* source, std::int64_t amount) { + switch (function) { + case Add::Ticks: return sceRtcTickAddTicks(destination, source, amount); + case Add::Microseconds: return sceRtcTickAddMicroseconds(destination, source, amount); + case Add::Seconds: return sceRtcTickAddSeconds(destination, source, amount); + case Add::Minutes: return sceRtcTickAddMinutes(destination, source, amount); + case Add::Hours: return sceRtcTickAddHours(destination, source, static_cast(amount)); + case Add::Days: return sceRtcTickAddDays(destination, source, static_cast(amount)); + case Add::Weeks: return sceRtcTickAddWeeks(destination, source, static_cast(amount)); + case Add::Months: return sceRtcTickAddMonths(destination, source, static_cast(amount)); + case Add::Years: return sceRtcTickAddYears(destination, source, static_cast(amount)); + } + std::abort(); +} + int main() { constexpr int invalidPointer = static_cast(0x80B50002); constexpr int invalidValue = static_cast(0x80B50003); @@ -431,12 +462,9 @@ int main() { Require(sceRtcGetTick(&endOfJanuary, &source) == 0); Require(sceRtcTickAddMonths(&result, &source, 1) == 0); Require(sceRtcSetTick(&converted, &result) == 0 && Equal(converted, RtcDateTime{2024, 2, 29, 8, 0, 0, 0})); - Require(sceRtcTickAddMonths(&result, &source, -12 * 2024) == invalidValue); source.tick = 5; - Require(sceRtcTickAddTicks(&result, &source, -6) == invalidValue); Require(sceRtcTickAddTicks(&result, &source, -5) == 0 && result.tick == 0); source.tick = maxTick; - Require(sceRtcTickAddTicks(&result, &source, 1) == invalidValue); Require(sceRtcTickAddTicks(nullptr, &source, 1) == invalidPointer); source.tick = leapDayTick; result.tick = 123; @@ -446,33 +474,8 @@ int main() { Require(sceRtcTickAddWeeks(&result, &source, 1) == 0 && result.tick == leapDayTick + 604800000000ull); Require(sceRtcSetTick(&converted, &result) == 0 && converted.month == 3 && converted.day == 7); result.tick = 123; - Require(sceRtcTickAddMinutes(&result, &source, std::numeric_limits::max()) == invalidValue && result.tick == 123); - Require(sceRtcTickAddWeeks(&result, &source, std::numeric_limits::max()) == invalidValue && result.tick == 123); Require(sceRtcTickAddHours(nullptr, &source, 1) == invalidPointer); Require(sceRtcTickAddMicroseconds(&result, nullptr, 1) == invalidPointer); - source.tick = maxTick; - Require(sceRtcTickAddHours(&result, &source, 1) == invalidValue && result.tick == 123); - Require(sceRtcTickAddMicroseconds(&result, &source, 1) == invalidValue && result.tick == 123); - source.tick = 0; - Require(sceRtcTickAddMinutes(&result, &source, -1) == invalidValue && result.tick == 123); - - const std::uint64_t invalidTicks[] = {maxTick + 1, std::numeric_limits::max()}; - for (std::uint64_t invalidTick : invalidTicks) { - source.tick = invalidTick; - result.tick = 123; - Require(sceRtcTickAddTicks(&result, &source, 0) == invalidValue && result.tick == 123); - Require(sceRtcTickAddTicks(&result, &source, 1) == invalidValue && result.tick == 123); - Require(sceRtcTickAddTicks(&result, &source, -1) == invalidValue && result.tick == 123); - Require(sceRtcTickAddSeconds(&result, &source, 0) == invalidValue && result.tick == 123); - Require(sceRtcTickAddDays(&result, &source, -1) == invalidValue && result.tick == 123); - Require(sceRtcTickAddMonths(&result, &source, 0) == invalidValue && result.tick == 123); - Require(sceRtcTickAddYears(&result, &source, -1) == invalidValue && result.tick == 123); - Require(sceRtcTickAddTicks(&source, &source, 0) == invalidValue && source.tick == invalidTick); - Require(sceRtcTickAddMonths(&source, &source, -1) == invalidValue && source.tick == invalidTick); - Require(sceRtcTickAddTicks(nullptr, &source, 0) == invalidPointer); - Require(sceRtcTickAddMonths(nullptr, &source, 0) == invalidPointer); - } - source.tick = maxTick; Require(sceRtcTickAddTicks(&source, &source, 0) == 0 && source.tick == maxTick); Require(sceRtcTickAddTicks(&source, &source, -1) == 0 && source.tick == maxTick - 1); @@ -480,13 +483,242 @@ int main() { Require(sceRtcTickAddMonths(&source, &source, 0) == 0 && source.tick == maxTick); Require(sceRtcTickAddYears(&result, &source, -1) == 0); Require(sceRtcSetTick(&converted, &result) == 0 && Equal(converted, RtcDateTime{9998, 12, 31, 23, 59, 59, 999999})); - result.tick = 123; - Require(sceRtcTickAddMonths(&result, &source, 1) == invalidValue && result.tick == 123); - Require(sceRtcTickAddTicks(&result, &source, std::numeric_limits::min()) == invalidValue && result.tick == 123); - Require(sceRtcTickAddSeconds(&result, &source, std::numeric_limits::max()) == invalidValue && result.tick == 123); source.tick = 0; Require(sceRtcTickAddMonths(&source, &source, 0) == 0 && source.tick == 0); - Require(sceRtcTickAddMonths(&result, &source, -1) == invalidValue && result.tick == 123); + + constexpr std::int64_t min64 = std::numeric_limits::min(); + constexpr std::int64_t max64 = std::numeric_limits::max(); + constexpr std::uint64_t addSentinel = 0xA5A5A5A5A5A5A5A5ull; + const AddCase addCases[] = { + {Add::Ticks, 0, -1, true, 0xffffffffffffffffull}, + {Add::Ticks, 1ull, -1, true, 0x0000000000000000ull}, + {Add::Ticks, 5, -1, true, 0x0000000000000004ull}, + {Add::Ticks, leapDayTick, 1, true, 0x00e2d2837bd62609ull}, + {Add::Ticks, maxTick - 1ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Ticks, maxTick, 1, true, 0x0461040bcb9f2000ull}, + {Add::Ticks, maxTick + 1, 1, true, 0x0461040bcb9f2001ull}, + {Add::Ticks, ~0ull, 1, true, 0x0000000000000000ull}, + {Add::Ticks, 0, max64, true, 0x7fffffffffffffffull}, + {Add::Ticks, 0, min64, true, 0x8000000000000000ull}, + {Add::Ticks, maxTick, max64, true, 0x8461040bcb9f1ffeull}, + {Add::Ticks, ~0ull, min64, true, 0x7fffffffffffffffull}, + {Add::Microseconds, 0, -1, true, 0xffffffffffffffffull}, + {Add::Microseconds, 1ull, -1, true, 0x0000000000000000ull}, + {Add::Microseconds, 5, -1, true, 0x0000000000000004ull}, + {Add::Microseconds, leapDayTick, 1, true, 0x00e2d2837bd62609ull}, + {Add::Microseconds, maxTick - 1ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Microseconds, maxTick, 1, true, 0x0461040bcb9f2000ull}, + {Add::Microseconds, maxTick + 1, 1, true, 0x0461040bcb9f2001ull}, + {Add::Microseconds, ~0ull, 1, true, 0x0000000000000000ull}, + {Add::Microseconds, 0, max64, true, 0x7fffffffffffffffull}, + {Add::Microseconds, 0, min64, true, 0x8000000000000000ull}, + {Add::Microseconds, maxTick, max64, true, 0x8461040bcb9f1ffeull}, + {Add::Microseconds, ~0ull, min64, true, 0x7fffffffffffffffull}, + {Add::Seconds, 0, -1, true, 0xfffffffffff0bdc0ull}, + {Add::Seconds, 1000000ull, -1, true, 0x0000000000000000ull}, + {Add::Seconds, 999999ull, -1, true, 0xffffffffffffffffull}, + {Add::Seconds, leapDayTick, 1, true, 0x00e2d2837be56848ull}, + {Add::Seconds, maxTick - 1000000ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Seconds, maxTick + 1 - 1000000ull, 1, true, 0x0461040bcb9f2000ull}, + {Add::Seconds, maxTick + 1, 1, true, 0x0461040bcbae6240ull}, + {Add::Seconds, 0ull - 1000000ull, 1, true, 0x0000000000000000ull}, + {Add::Seconds, 0, max64, true, 0xfffffffffff0bdc0ull}, + {Add::Seconds, 0, min64, true, 0x0000000000000000ull}, + {Add::Seconds, maxTick, max64, true, 0x0461040bcb8fddbfull}, + {Add::Seconds, ~0ull, min64, true, 0xffffffffffffffffull}, + {Add::Minutes, 0, -1, true, 0xfffffffffc6c7900ull}, + {Add::Minutes, 60000000ull, -1, true, 0x0000000000000000ull}, + {Add::Minutes, 59999999ull, -1, true, 0xffffffffffffffffull}, + {Add::Minutes, leapDayTick, 1, true, 0x00e2d2837f69ad08ull}, + {Add::Minutes, maxTick - 60000000ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Minutes, maxTick + 1 - 60000000ull, 1, true, 0x0461040bcb9f2000ull}, + {Add::Minutes, maxTick + 1, 1, true, 0x0461040bcf32a700ull}, + {Add::Minutes, 0ull - 60000000ull, 1, true, 0x0000000000000000ull}, + {Add::Minutes, 0, max64, true, 0xfffffffffc6c7900ull}, + {Add::Minutes, 0, min64, true, 0x0000000000000000ull}, + {Add::Minutes, maxTick, max64, true, 0x0461040bc80b98ffull}, + {Add::Minutes, ~0ull, min64, true, 0xffffffffffffffffull}, + {Add::Hours, 0, -1, true, 0xffffffff296c5c00ull}, + {Add::Hours, 3600000000ull, -1, true, 0x0000000000000000ull}, + {Add::Hours, 3599999999ull, -1, true, 0xffffffffffffffffull}, + {Add::Hours, leapDayTick, 1, true, 0x00e2d2845269ca08ull}, + {Add::Hours, maxTick - 3600000000ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Hours, maxTick + 1 - 3600000000ull, 1, true, 0x0461040bcb9f2000ull}, + {Add::Hours, maxTick + 1, 1, true, 0x0461040ca232c400ull}, + {Add::Hours, 0ull - 3600000000ull, 1, true, 0x0000000000000000ull}, + {Add::Hours, 0, 2147483647, true, 0x6b49d1ff296c5c00ull}, + {Add::Hours, 0, -2147483648, true, 0x94b62e0000000000ull}, + {Add::Hours, maxTick, 2147483647, true, 0x6faad60af50b7bffull}, + {Add::Hours, ~0ull, -2147483648, true, 0x94b62dffffffffffull}, + {Add::Days, 0, -1, true, 0xffffffebe228a000ull}, + {Add::Days, 86400000000ull, -1, true, 0x0000000000000000ull}, + {Add::Days, 86399999999ull, -1, true, 0xffffffffffffffffull}, + {Add::Days, leapDayTick, 1, true, 0x00e2d29799ad8608ull}, + {Add::Days, maxTick - 86400000000ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Days, maxTick + 1 - 86400000000ull, 1, true, 0x0461040bcb9f2000ull}, + {Add::Days, maxTick + 1, 1, true, 0x0461041fe9768000ull}, + {Add::Days, 0ull - 86400000000ull, 1, true, 0x0000000000000000ull}, + {Add::Days, 0, 2147483647, true, 0x0eebafebe228a000ull}, + {Add::Days, 0, -2147483648, true, 0xf114500000000000ull}, + {Add::Days, maxTick, 2147483647, true, 0x134cb3f7adc7bfffull}, + {Add::Days, ~0ull, -2147483648, true, 0xf1144fffffffffffull}, + {Add::Weeks, 0, -1, true, 0xffffff732f1c6000ull}, + {Add::Weeks, 604800000000ull, -1, true, 0x0000000000000000ull}, + {Add::Weeks, 604799999999ull, -1, true, 0xffffffffffffffffull}, + {Add::Weeks, leapDayTick, 1, true, 0x00e2d3104cb9c608ull}, + {Add::Weeks, maxTick - 604800000000ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Weeks, maxTick + 1 - 604800000000ull, 1, true, 0x0461040bcb9f2000ull}, + {Add::Weeks, maxTick + 1, 1, true, 0x046104989c82c000ull}, + {Add::Weeks, 0ull - 604800000000ull, 1, true, 0x0000000000000000ull}, + {Add::Weeks, 0, 2147483647, true, 0x6871cf732f1c6000ull}, + {Add::Weeks, 0, -2147483648, true, 0x978e300000000000ull}, + {Add::Weeks, maxTick, 2147483647, true, 0x6cd2d37efabb7fffull}, + {Add::Weeks, ~0ull, -2147483648, true, 0x978e2fffffffffffull}, + {Add::Months, 0, -1, false, 0}, + {Add::Months, 0, 0, true, 0x0000000000000000ull}, + {Add::Months, 0, 1, true, 0x0000026f9d14a000ull}, + {Add::Months, 0, -12, false, 0}, + {Add::Months, 0, 12, true, 0x00001cae8c13e000ull}, + {Add::Months, 0, 119988, false, 0}, + {Add::Months, 0, 2147483647, false, 0}, + {Add::Months, 0, -2147483648, false, 0}, + {Add::Months, 0, -1239376452, true, 0x017942765fc16000ull}, + {Add::Months, 0, -1239376451, false, 0}, + {Add::Months, at(1, 2, 1, 0, 0, 0), -1, true, 0x0000000000000000ull}, + {Add::Months, at(1, 2, 1, 0, 0, 0), 0, true, 0x0000026f9d14a000ull}, + {Add::Months, at(1, 2, 1, 0, 0, 0), 1, true, 0x000004a2e0a32000ull}, + {Add::Months, at(1, 2, 1, 0, 0, 0), -12, false, 0}, + {Add::Months, at(1, 2, 1, 0, 0, 0), 12, true, 0x00001f1e29288000ull}, + {Add::Months, at(1, 2, 1, 0, 0, 0), 119988, false, 0}, + {Add::Months, at(1, 2, 1, 0, 0, 0), 2147483647, false, 0}, + {Add::Months, at(1, 2, 1, 0, 0, 0), -2147483648, false, 0}, + {Add::Months, at(1, 2, 1, 0, 0, 0), -1239376452, false, 0}, + {Add::Months, at(1, 2, 1, 0, 0, 0), -1239376451, false, 0}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), -1, true, 0x00e2cdc8a612a000ull}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), 0, true, 0x00e2d03843274000ull}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), 1, true, 0x00e2d27fa48d2000ull}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), -12, true, 0x00e2b389b7136000ull}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), 12, true, 0x00e2ecfaed128000ull}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), 119988, false, 0}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), 2147483647, false, 0}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), -2147483648, false, 0}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), -1239376452, true, 0x025c12c2c0c00000ull}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), -1239376451, false, 0}, + {Add::Months, leapDayTick, -1, true, 0x00e2d013dec18608ull}, + {Add::Months, leapDayTick, 0, true, 0x00e2d2837bd62608ull}, + {Add::Months, leapDayTick, 1, true, 0x00e2d4cadd3c0608ull}, + {Add::Months, leapDayTick, -12, true, 0x00e2b5c0d1eae608ull}, + {Add::Months, leapDayTick, 12, true, 0x00e2ef3207ea0608ull}, + {Add::Months, leapDayTick, 119988, false, 0}, + {Add::Months, leapDayTick, 2147483647, false, 0}, + {Add::Months, leapDayTick, -2147483648, false, 0}, + {Add::Months, leapDayTick, -1239376452, false, 0}, + {Add::Months, leapDayTick, -1239376451, false, 0}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), -1, true, 0x0460e4eda276a000ull}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), 0, true, 0x0460e75d3f8b4000ull}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), 1, true, 0x0460e9ccdc9fe000ull}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), -12, true, 0x0460caaeb3776000ull}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), 12, false, 0}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), 119988, false, 0}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), 2147483647, false, 0}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), -2147483648, false, 0}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), -1239376452, false, 0}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), -1239376451, false, 0}, + {Add::Months, maxTick, -1, true, 0x0461019c2e8a7fffull}, + {Add::Months, maxTick, 0, true, 0x0461040bcb9f1fffull}, + {Add::Months, maxTick, 1, false, 0}, + {Add::Months, maxTick, -12, true, 0x0460e75d3f8b3fffull}, + {Add::Months, maxTick, 12, false, 0}, + {Add::Months, maxTick, 119988, false, 0}, + {Add::Months, maxTick, 2147483647, false, 0}, + {Add::Months, maxTick, -2147483648, false, 0}, + {Add::Months, maxTick, -1239376452, false, 0}, + {Add::Months, maxTick, -1239376451, false, 0}, + {Add::Months, maxTick + 1, -1, true, 0x0461019c2e8a8000ull}, + {Add::Months, maxTick + 1, 0, false, 0}, + {Add::Months, maxTick + 1, 1, false, 0}, + {Add::Months, maxTick + 1, -12, true, 0x0460e75d3f8b4000ull}, + {Add::Months, maxTick + 1, 12, false, 0}, + {Add::Months, maxTick + 1, 119988, false, 0}, + {Add::Months, maxTick + 1, 2147483647, false, 0}, + {Add::Months, maxTick + 1, -2147483648, false, 0}, + {Add::Months, maxTick + 1, -1239376452, false, 0}, + {Add::Months, maxTick + 1, -1239376451, false, 0}, + {Add::Months, ~0ull, -1, false, 0}, + {Add::Months, ~0ull, 0, false, 0}, + {Add::Months, ~0ull, 1, false, 0}, + {Add::Months, ~0ull, -12, false, 0}, + {Add::Months, ~0ull, 12, false, 0}, + {Add::Months, ~0ull, 119988, true, 0x02122fd805921fffull}, + {Add::Months, ~0ull, 2147483647, false, 0}, + {Add::Months, ~0ull, -2147483648, false, 0}, + {Add::Months, ~0ull, -1239376452, false, 0}, + {Add::Months, ~0ull, -1239376451, false, 0}, + {Add::Years, 0, -1, false, 0}, + {Add::Years, 0, 0, true, 0x0000000000000000ull}, + {Add::Years, 0, 1, true, 0x00001cae8c13e000ull}, + {Add::Years, 0, 9998, true, 0x0460e75d3f8b4000ull}, + {Add::Years, 0, 32767, false, 0}, + {Add::Years, 0, -32768, false, 0}, + {Add::Years, at(1, 2, 1, 0, 0, 0), -1, false, 0}, + {Add::Years, at(1, 2, 1, 0, 0, 0), 0, true, 0x0000026f9d14a000ull}, + {Add::Years, at(1, 2, 1, 0, 0, 0), 1, true, 0x00001f1e29288000ull}, + {Add::Years, at(1, 2, 1, 0, 0, 0), 9998, true, 0x0460e9ccdc9fe000ull}, + {Add::Years, at(1, 2, 1, 0, 0, 0), 32767, false, 0}, + {Add::Years, at(1, 2, 1, 0, 0, 0), -32768, false, 0}, + {Add::Years, at(2024, 1, 31, 8, 0, 0), -1, true, 0x00e2b389b7136000ull}, + {Add::Years, at(2024, 1, 31, 8, 0, 0), 0, true, 0x00e2d03843274000ull}, + {Add::Years, at(2024, 1, 31, 8, 0, 0), 1, true, 0x00e2ecfaed128000ull}, + {Add::Years, at(2024, 1, 31, 8, 0, 0), 9998, false, 0}, + {Add::Years, at(2024, 1, 31, 8, 0, 0), 32767, false, 0}, + {Add::Years, at(2024, 1, 31, 8, 0, 0), -32768, false, 0}, + {Add::Years, leapDayTick, -1, true, 0x00e2b5c0d1eae608ull}, + {Add::Years, leapDayTick, 0, true, 0x00e2d2837bd62608ull}, + {Add::Years, leapDayTick, 1, true, 0x00e2ef3207ea0608ull}, + {Add::Years, leapDayTick, 9998, false, 0}, + {Add::Years, leapDayTick, 32767, false, 0}, + {Add::Years, leapDayTick, -32768, false, 0}, + {Add::Years, at(9999, 1, 1, 0, 0, 0), -1, true, 0x0460caaeb3776000ull}, + {Add::Years, at(9999, 1, 1, 0, 0, 0), 0, true, 0x0460e75d3f8b4000ull}, + {Add::Years, at(9999, 1, 1, 0, 0, 0), 1, false, 0}, + {Add::Years, at(9999, 1, 1, 0, 0, 0), 9998, false, 0}, + {Add::Years, at(9999, 1, 1, 0, 0, 0), 32767, false, 0}, + {Add::Years, at(9999, 1, 1, 0, 0, 0), -32768, false, 0}, + {Add::Years, maxTick, -1, true, 0x0460e75d3f8b3fffull}, + {Add::Years, maxTick, 0, true, 0x0461040bcb9f1fffull}, + {Add::Years, maxTick, 1, false, 0}, + {Add::Years, maxTick, 9998, false, 0}, + {Add::Years, maxTick, 32767, false, 0}, + {Add::Years, maxTick, -32768, false, 0}, + {Add::Years, maxTick + 1, -1, true, 0x0460e75d3f8b4000ull}, + {Add::Years, maxTick + 1, 0, false, 0}, + {Add::Years, maxTick + 1, 1, false, 0}, + {Add::Years, maxTick + 1, 9998, false, 0}, + {Add::Years, maxTick + 1, 32767, false, 0}, + {Add::Years, maxTick + 1, -32768, false, 0}, + {Add::Years, ~0ull, -1, false, 0}, + {Add::Years, ~0ull, 0, false, 0}, + {Add::Years, ~0ull, 1, false, 0}, + {Add::Years, ~0ull, 9998, true, 0x02121329797e3fffull}, + {Add::Years, ~0ull, 32767, false, 0}, + {Add::Years, ~0ull, -32768, false, 0}, + }; + for (const AddCase& entry : addCases) { + RtcTick destination{addSentinel}; + const RtcTick origin{entry.source}; + Require(CallAdd(entry.function, &destination, &origin, entry.amount) == 0); + Require(destination.tick == (entry.written ? entry.result : addSentinel)); + RtcTick inPlace{entry.source}; + Require(CallAdd(entry.function, &inPlace, &inPlace, entry.amount) == 0); + Require(inPlace.tick == (entry.written ? entry.result : entry.source)); + } + for (Add function : {Add::Ticks, Add::Microseconds, Add::Seconds, Add::Minutes, Add::Hours, Add::Days, Add::Weeks, Add::Months, Add::Years}) { + RtcTick destination{addSentinel}; + const RtcTick origin{5}; + Require(CallAdd(function, nullptr, &origin, 1) == invalidPointer); + Require(CallAdd(function, &destination, nullptr, 1) == invalidPointer && destination.tick == addSentinel); + Require(CallAdd(function, nullptr, nullptr, 1) == invalidPointer); + } RtcTick now{}; Require(sceRtcGetCurrentTick(&now) == 0 && now.tick > leapDayTick); @@ -613,5 +845,23 @@ int main() { Require(sceRtcGetCurrentAdNetworkTick(nullptr) == invalidPointer); RtcTick tooEarly{unixEpochTick - 1}; Require(sceRtcSetCurrentTick(&tooEarly) == invalidValue && sceRtcSetCurrentTick(nullptr) == invalidPointer); + const RtcTick systemClock{leapDayTick}; + const RtcTick networkClock{leapDayTick + 10000000}; + const RtcTick adClock{leapDayTick + 20000000}; + const RtcTick debugClock{leapDayTick + 30000000}; + Require(sceRtcSetCurrentTick(&systemClock) == 0); + Require(sceRtcSetCurrentNetworkTick(&networkClock) == 0); + Require(sceRtcGetCurrentAdNetworkTick(¤t) == 0 && current.tick >= networkClock.tick && current.tick < networkClock.tick + 1000000); + Require(sceRtcGetCurrentDebugNetworkTick(¤t) == 0 && current.tick >= networkClock.tick && current.tick < networkClock.tick + 1000000); + Require(sceRtcSetCurrentAdNetworkTick(&adClock) == 0); + Require(sceRtcSetCurrentDebugNetworkTick(&debugClock) == 0); + Require(sceRtcSetCurrentNetworkTick(&tooEarly) == invalidValue && sceRtcSetCurrentNetworkTick(nullptr) == invalidPointer); + Require(sceRtcSetCurrentAdNetworkTick(&tooEarly) == invalidValue && sceRtcSetCurrentAdNetworkTick(nullptr) == invalidPointer); + Require(sceRtcSetCurrentDebugNetworkTick(&tooEarly) == invalidValue && sceRtcSetCurrentDebugNetworkTick(nullptr) == invalidPointer); + Require(sceRtcGetCurrentTick(¤t) == 0 && current.tick >= systemClock.tick && current.tick < systemClock.tick + 1000000); + Require(sceRtcGetCurrentNetworkTick(¤t) == 0 && current.tick >= networkClock.tick && current.tick < networkClock.tick + 1000000); + Require(sceRtcGetCurrentAdNetworkTick(¤t) == 0 && current.tick >= adClock.tick && current.tick < adClock.tick + 1000000); + Require(sceRtcGetCurrentDebugNetworkTick(¤t) == 0 && current.tick >= debugClock.tick && current.tick < debugClock.tick + 1000000); + Require(sceRtcGetCurrentRawNetworkTick(¤t) == 0 && current.tick > unixEpochTick); } } diff --git a/core/libs/tests/GuestRudpPoll.cpp b/core/libs/tests/GuestRudpPoll.cpp new file mode 100644 index 0000000000..d797fad444 --- /dev/null +++ b/core/libs/tests/GuestRudpPoll.cpp @@ -0,0 +1,124 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include +#include + +extern "C" { +using GuestRudpEventHandler = void(APS5_VABI*)(int, int, int, void*); +struct GuestRudpPollEvent { + int ctxId; + std::uint16_t reqEvents; + std::uint16_t rtnEvents; +}; +int APS5_VABI sceRudpInit_nid_postfix(void*, int); +int APS5_VABI sceRudpCreateContext(GuestRudpEventHandler, void*, int*); +int APS5_VABI sceRudpTerminate(int); +int APS5_VABI sceRudpEnd(); +int APS5_VABI sceRudpPollCreate(std::uint32_t); +int APS5_VABI sceRudpPollControl(int, int, int, std::uint16_t); +int APS5_VABI sceRudpPollWait(int, GuestRudpPollEvent*, std::uint32_t, std::uint64_t); +int APS5_VABI sceRudpPollDestroy(int); +} + +static void Require(bool value) { + if (!value) std::abort(); +} + +static void APS5_VABI Handler(int, int, int, void*) {} + +int main() { + constexpr int notInitialized = static_cast(0x80770001u); + constexpr int invalidContext = static_cast(0x80770003u); + constexpr int invalidArgument = static_cast(0x80770004u); + constexpr int invalidPoll = static_cast(0x8077001Fu); + constexpr int tooManyContexts = static_cast(0x80770020u); + constexpr int add = 1; + constexpr int modify = 2; + constexpr int remove = 3; + constexpr std::uint16_t read = 0x0001; + constexpr std::uint16_t write = 0x0002; + constexpr std::uint64_t forever = std::numeric_limits::max(); + alignas(16) static std::array pool{}; + std::array events{}; + const auto eventCount = static_cast(events.size()); + + Require(sceRudpPollCreate(1) == notInitialized); + Require(sceRudpPollControl(1, add, 0, read) == notInitialized); + Require(sceRudpPollWait(1, events.data(), eventCount, 0) == notInitialized); + Require(sceRudpPollDestroy(1) == notInitialized); + Require(sceRudpInit_nid_postfix(pool.data(), static_cast(pool.size())) == 0); + + Require(sceRudpPollCreate(0) == invalidArgument); + const int poll = sceRudpPollCreate(1); + Require(poll > 0); + const int other = sceRudpPollCreate(2); + Require(other > 0 && other != poll); + + int first = -1; + int second = -1; + Require(sceRudpCreateContext(Handler, nullptr, &first) == 0); + Require(sceRudpCreateContext(Handler, nullptr, &second) == 0); + + Require(sceRudpPollControl(poll, 0, first, read) == invalidArgument); + Require(sceRudpPollControl(poll, 4, first, read) == invalidArgument); + Require(sceRudpPollControl(-1, add, first, read) == invalidArgument); + Require(sceRudpPollControl(poll, add, -1, read) == invalidArgument); + Require(sceRudpPollControl(12345, add, first, read) == invalidPoll); + Require(sceRudpPollControl(poll, add, 12345, read) == invalidContext); + Require(sceRudpPollControl(poll, modify, first, read) == invalidContext); + Require(sceRudpPollControl(poll, remove, first, read) == invalidContext); + + Require(sceRudpPollControl(poll, add, first, read) == 0); + Require(sceRudpPollControl(poll, add, first, read) == tooManyContexts); + Require(sceRudpPollControl(poll, add, second, read) == tooManyContexts); + Require(sceRudpPollControl(other, add, first, read) == 0); + Require(sceRudpPollControl(other, add, first, read) == invalidContext); + Require(sceRudpPollControl(poll, modify, first, read | write) == 0); + Require(sceRudpPollControl(poll, remove, first, 0) == 0); + Require(sceRudpPollControl(poll, remove, first, 0) == invalidContext); + Require(sceRudpPollControl(poll, add, second, write) == 0); + + Require(sceRudpPollWait(-1, events.data(), eventCount, 0) == invalidArgument); + Require(sceRudpPollWait(poll, nullptr, eventCount, 0) == invalidArgument); + Require(sceRudpPollWait(poll, events.data(), 0, 0) == invalidArgument); + Require(sceRudpPollWait(12345, events.data(), eventCount, 0) == invalidPoll); + Require(sceRudpPollWait(poll, events.data(), eventCount, 0) == 0); + const auto start = std::chrono::steady_clock::now(); + Require(sceRudpPollWait(poll, events.data(), eventCount, 20000) == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(20)); + + Require(sceRudpTerminate(second) == 0); + Require(sceRudpPollControl(poll, add, first, read) == 0); + Require(sceRudpTerminate(first) == 0); + Require(sceRudpPollControl(poll, add, first, read) == invalidContext); + int third = -1; + Require(sceRudpCreateContext(Handler, nullptr, &third) == 0); + Require(sceRudpPollControl(poll, add, third, read) == 0); + Require(sceRudpPollControl(other, add, third, read) == 0); + + int destroyedResult = 0; + std::thread destroyedWaiter([&] { destroyedResult = sceRudpPollWait(poll, events.data(), eventCount, forever); }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(sceRudpPollDestroy(poll) == 0); + destroyedWaiter.join(); + Require(destroyedResult == invalidPoll); + Require(sceRudpPollDestroy(poll) == invalidPoll); + Require(sceRudpPollDestroy(-1) == invalidArgument); + Require(sceRudpPollControl(poll, add, third, read) == invalidPoll); + + int endedResult = 0; + std::thread endedWaiter([&] { endedResult = sceRudpPollWait(other, events.data(), eventCount, forever); }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(sceRudpEnd() == 0); + endedWaiter.join(); + Require(endedResult == notInitialized); + + Require(sceRudpInit_nid_postfix(pool.data(), static_cast(pool.size())) == 0); + Require(sceRudpPollDestroy(other) == invalidPoll); + Require(sceRudpEnd() == 0); + return 0; +} diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index 92571b6b8c..4fb5ea120e 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -14,6 +14,7 @@ int APS5_VABI sceSaveDataMount3(const SaveDataMount3*, SaveDataMountResult*); int APS5_VABI sceSaveDataUmount2(std::uint32_t, const SaveDataMountPoint*); int APS5_VABI sceSaveDataGetMountInfo(const SaveDataMountPoint*, SaveDataMountInfo*); int APS5_VABI sceSaveDataGetParam(const SaveDataMountPoint*, std::uint32_t, void*, std::size_t, std::size_t*); +int APS5_VABI sceSaveDataSetParam(const SaveDataMountPoint*, std::uint32_t, const void*, std::size_t); } namespace { @@ -46,6 +47,9 @@ void CheckUnmountedCalls() { Check(sceSaveDataGetParam(nullptr, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null point is PARAMETER"); Check(sceSaveDataGetParam(&point, 1, nullptr, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null buf is PARAMETER"); Check(sceSaveDataGetParam(&point, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorNotMounted, "getParam unmounted point is NOT_MOUNTED"); + Check(sceSaveDataSetParam(nullptr, 1, buf, sizeof(buf)) == SaveDataErrorParameter, "setParam null point is PARAMETER"); + Check(sceSaveDataSetParam(&point, 1, nullptr, sizeof(buf)) == SaveDataErrorParameter, "setParam null buf is PARAMETER"); + Check(sceSaveDataSetParam(&point, 1, buf, sizeof(buf)) == SaveDataErrorNotMounted, "setParam unmounted point is NOT_MOUNTED"); } void CheckMountInfoAndParam(const SaveDataMountPoint* point) { @@ -55,6 +59,8 @@ void CheckMountInfoAndParam(const SaveDataMountPoint* point) { std::size_t gotSize = 0; SaveDataMountPoint unmounted{}; std::memcpy(unmounted.data, "unmounted", sizeof("unmounted")); + const char newTitle[] = "Mounted Title"; + char readTitle[128]{}; Check(sceSaveDataGetMountInfo(nullptr, &info) == SaveDataErrorParameter, "getMountInfo null point is PARAMETER"); Check(sceSaveDataGetMountInfo(point, nullptr) == SaveDataErrorParameter, "getMountInfo null info is PARAMETER"); Check(sceSaveDataGetMountInfo(&unmounted, &info) == SaveDataErrorNotMounted, "getMountInfo unmounted point is NOT_MOUNTED"); @@ -66,6 +72,15 @@ void CheckMountInfoAndParam(const SaveDataMountPoint* point) { Check(sceSaveDataGetParam(point, 1, buf, sizeof(buf), &gotSize) == 0, "getParam on mounted save succeeds"); Check(gotSize == sizeof(buf), "getParam reports expected size"); Check(sceSaveDataGetParam(point, 1, buf, sizeof(buf), nullptr) == 0, "getParam with null got_size on mounted save succeeds"); + Check(sceSaveDataSetParam(nullptr, 1, newTitle, sizeof(newTitle)) == SaveDataErrorParameter, "setParam null point is PARAMETER"); + Check(sceSaveDataSetParam(point, 1, nullptr, sizeof(newTitle)) == SaveDataErrorParameter, "setParam null buf is PARAMETER"); + Check(sceSaveDataSetParam(point, 999, newTitle, sizeof(newTitle)) == SaveDataErrorParameter, "setParam invalid type is PARAMETER"); + Check(sceSaveDataSetParam(point, 1, newTitle, 0) == SaveDataErrorParameter, "setParam zero size is PARAMETER"); + Check(sceSaveDataSetParam(point, 4, smallBuf, 1) == SaveDataErrorParameter, "setParam short user param is PARAMETER"); + Check(sceSaveDataSetParam(&unmounted, 1, newTitle, sizeof(newTitle)) == SaveDataErrorNotMounted, "setParam unmounted point is NOT_MOUNTED"); + Check(sceSaveDataSetParam(point, 1, newTitle, sizeof(newTitle)) == 0, "setParam on mounted save succeeds"); + Check(sceSaveDataGetParam(point, 1, readTitle, sizeof(readTitle), &gotSize) == 0, "getParam reads back set title"); + Check(std::strcmp(readTitle, newTitle) == 0, "readTitle matches newTitle"); } void MountAndCheck(const char* name, std::uint32_t mode, std::uint32_t expectedStatus, const std::string& what) { diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index e24e2a4928..6e2b621d33 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -5,6 +5,7 @@ #include #else #include +#include #include #include #include @@ -25,6 +26,7 @@ #include #include #include +#include extern "C" { extern const std::uint8_t in6addr_any_nid_postfix[16]; @@ -38,6 +40,7 @@ int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI sceNetListen(int, int); int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); int APS5_VABI sceNetGetpeername(int, void*, std::uint32_t*); +int APS5_VABI sceNetGetSockInfo(int, void*, int, int); int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); int APS5_VABI sceNetAccept(int, void*, std::uint32_t*); std::int64_t APS5_VABI sceNetSend(int, const void*, std::size_t, int); @@ -64,9 +67,11 @@ int APS5_VABI sceNetResolverStartNtoa6(int, const char*, void*, int, int, int); int APS5_VABI sceNetResolverStartNtoaMultipleRecordsEx(int, const char*, void*, int, int, int); int APS5_VABI sceNetResolverStartNtoaMultipleRecords(int, const char*, void*, int, int, int); int APS5_VABI sceNetResolverStartAton(int, const void*, char*, int, int, int, int); +int APS5_VABI sceNetResolverStartAton6(int, const void*, char*, int, int, int, int); int APS5_VABI sceNetResolverAbort(int, int); int APS5_VABI sceNetResolverDestroy(int); int APS5_VABI sceNetResolverGetError(int, int*); +int APS5_VABI sceNetClearDnsCache(int); int APS5_VABI sceNetCtlGetState(int*); int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat*); int APS5_VABI sceNetCtlRegisterCallback(NetCtlCallback, void*, int*); @@ -124,6 +129,10 @@ std::int64_t APS5_VABI sceNetRecvmsg(int, NetMsghdr*, int); int APS5_VABI sceNetPoolCreate(const char*, int, int); int APS5_VABI sceNetPoolDestroy(int); int APS5_VABI sceNetGetMemoryPoolStats(int, NetMemoryPoolStats*); +const char* APS5_VABI sceNetGetIfName(std::uint32_t); +int APS5_VABI sceNetGetRandom(std::uint32_t*); +void* APS5_VABI sceNetMemoryAllocate(std::int64_t, int); +void APS5_VABI sceNetMemoryFree(void*); } static void Require(bool condition) { @@ -260,6 +269,50 @@ static void CheckPoolStats() { Require(Failed(sceNetGetMemoryPoolStats(pool, &stats), 9)); } +static void CheckIfName() { + constexpr std::array names = { + "lo0", "eth0", "eth1", "dbg0", "wlan0", "wlan1", "gbe0", "bt0", "phone0", "pppoe0"}; + for (std::uint32_t index = 0; index < names.size(); ++index) { + Require(std::strcmp(sceNetGetIfName(index), names[index]) == 0); + } + *sceNetErrnoLoc() = 0; + const char* missing = sceNetGetIfName(10); + Require(missing != nullptr && missing[0] == '\0' && *sceNetErrnoLoc() == 22); + *sceNetErrnoLoc() = 0; + missing = sceNetGetIfName(0xffffffffu); + Require(missing != nullptr && missing[0] == '\0' && *sceNetErrnoLoc() == 22); +} + +static void CheckRandom() { + Require(Failed(sceNetGetRandom(nullptr), 22)); + std::array values{}; + for (auto& value : values) Require(sceNetGetRandom(&value) == 0); + Require(std::any_of(values.begin() + 1, values.end(), [&](std::uint32_t value) { return value != values[0]; })); +} + +static void CheckNetMemory() { + Require(sceNetMemoryAllocate(0, 0) == nullptr); + Require(sceNetMemoryAllocate(-1, 2) == nullptr); + auto* zeroed = static_cast(sceNetMemoryAllocate(4096, 2)); + Require(zeroed != nullptr); + Require(std::all_of(zeroed, zeroed + 4096, [](std::uint8_t byte) { return byte == 0; })); + std::memset(zeroed, 0xa5, 4096); + auto* plain = static_cast(sceNetMemoryAllocate(64, 0)); + Require(plain != nullptr && plain != zeroed); + std::memset(plain, 0x5a, 64); + Require(zeroed[4095] == 0xa5); + sceNetMemoryFree(plain); + sceNetMemoryFree(zeroed); + sceNetMemoryFree(nullptr); + bool threw = false; + try { + sceNetMemoryAllocate(16, 1); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); +} + static void CheckUnboundSocketName() { for (const int family : {2, 28}) { for (const int type : {1, 2}) { @@ -466,13 +519,15 @@ static void CheckDontWait() { char buffer[16]{}; NetIovec in[] = {{buffer, sizeof(buffer)}}; NetMsghdr receive{nullptr, 0, in, 1, nullptr, 0, 0}; -#ifdef _WIN32 - Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait), 45)); - Require(Failed(sceNetRecvfrom(receiver, buffer, sizeof(buffer), DontWait, nullptr, nullptr), 45)); - Require(Failed(sceNetRecvmsg(receiver, &receive, DontWait), 45)); - Require(Failed(sceNetSendto(sender, "x", 1, DontWait, address.data(), address.size()), 45)); -#else const auto start = std::chrono::steady_clock::now(); + const int unbound = sceNetSocket("dontwait-unbound", 2, 2, 17); + Require(unbound >= 0); + const auto unboundStart = std::chrono::steady_clock::now(); + Require(Failed(sceNetRecv(unbound, buffer, sizeof(buffer), DontWait), 35)); + Require(Failed(sceNetRecvfrom(unbound, buffer, sizeof(buffer), DontWait, nullptr, nullptr), 35)); + Require(Failed(sceNetRecvmsg(unbound, &receive, DontWait), 35)); + Require(std::chrono::steady_clock::now() - unboundStart < std::chrono::seconds(1)); + Require(sceNetSocketClose(unbound) == 0); Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait), 35)); Require(Failed(sceNetRecvfrom(receiver, buffer, sizeof(buffer), DontWait, nullptr, nullptr), 35)); Require(Failed(sceNetRecvmsg(receiver, &receive, DontWait), 35)); @@ -489,7 +544,6 @@ static void CheckDontWait() { Require(sceNetSend(sender, datagram, sizeof(datagram), DontWait) == sizeof(datagram)); Require(sceNetRecvmsg(receiver, &receive, 0) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait), 35)); -#endif Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), 0x1), 45)); Require(Failed(sceNetSend(sender, "x", 1, Peek), 45)); Require(sceNetSocketClose(receiver) == 0 && sceNetSocketClose(sender) == 0); @@ -743,6 +797,48 @@ static void CheckInetPtonWhitespace() { } } +struct AtonCase { + const char* text; + std::uint32_t address; +}; + +static void CheckInetPtonExParts() { + for (const AtonCase& c : {AtonCase{"0", 0}, AtonCase{"1", 1}, AtonCase{"4294967295", 0xffffffff}, AtonCase{"4294967296", 0}, + AtonCase{"0xffffffff", 0xffffffff}, AtonCase{"0x100000000", 0}, AtonCase{"1.16777215", 0x01ffffff}, + AtonCase{"1.2.65535", 0x0102ffff}, AtonCase{"1.2.3.255", 0x010203ff}, AtonCase{"0x7F.1", 0x7f000001}, + AtonCase{"0X7f.1", 0x7f000001}, AtonCase{"0x", 0}, AtonCase{"0x.1.2.3", 0x00010203}, AtonCase{"0x8.1", 0x08000001}, AtonCase{"0x9", 9}, AtonCase{"1.2.3", 0x01020003}, + AtonCase{"127.1", 0x7f000001}, AtonCase{"010.0.0.1", 0x08000001}, AtonCase{"0377.1", 0xff000001}, + AtonCase{"00000000001.2.3.4", 0x01020304}, AtonCase{"1.2.3.4 x", 0x01020304}, AtonCase{"1.2.3.4\tx", 0x01020304}, + AtonCase{"1.2.3.4\v", 0x01020304}, AtonCase{"1 .2.3.4", 1}, AtonCase{"0.8", 8}, AtonCase{"00.1", 1}, + AtonCase{"09", 9}, AtonCase{"09.1", 0x09000001}, AtonCase{"1.09", 0x01000009}, AtonCase{"0.09", 9}, + AtonCase{"09.09", 0x09000009}, AtonCase{"0919", 919}, AtonCase{"0989", 989}, AtonCase{"0998", 998}, + AtonCase{"1.4294967296", 0x01000000}, AtonCase{"4294967297.1", 0x01000001}, AtonCase{"1.2.3.4294967297", 0x01020301}, + AtonCase{"18446744073709551617", 1}, AtonCase{"0x1ffffffff", 0xffffffff}, AtonCase{"0x100000001.1", 0x01000001}, + AtonCase{"1.0xffffff", 0x01ffffff}, AtonCase{"1.2.0xffff", 0x0102ffff}}) { + std::array address{}; + address.fill(0xaa); + *sceNetErrnoLoc() = 123; + Require(sceNetInetPtonEx(2, c.text, address.data(), 0) == 1 && *sceNetErrnoLoc() == 123); + Require(address[0] == (c.address >> 24) && address[1] == ((c.address >> 16) & 0xff) && address[2] == ((c.address >> 8) & 0xff) && + address[3] == (c.address & 0xff) && address[4] == 0xaa); + } + for (const char* text : {"", "1.16777216", "1.2.65536", "1.2.3.256", "256.1", "0400.1", "0xg.1", "08", "018", "019", "0199", "00919", + "080", "08.1.2.3", "1.08", "0.08", "0919.1", "09x", "1.0x1000000", "1.2.0x10000", "1.2.3.4.5", "1..2.3", + ".1.2.3", "1.2.3.", "1.2.3.4x", " 1.2.3.4", "1.2.3.4\x01", "1.2.3.4\xa0", "+1.2.3.4", "-1.2.3.4"}) { + std::array address{}; + address.fill(0xaa); + *sceNetErrnoLoc() = 123; + Require(sceNetInetPtonEx(2, text, address.data(), 0) == 0 && *sceNetErrnoLoc() == 123); + Require(std::all_of(address.begin(), address.end(), [](std::uint8_t byte) { return byte == 0xaa; })); + } + for (const char* text : {"::1 ", "::1x", " ::1", "1.2.3.4", "::1.2.3", "1:2:3:4:5:6:7:8:9"}) { + std::array address{}; + address.fill(0xaa); + Require(sceNetInetPtonEx(28, text, address.data(), 0) == 0); + Require(std::all_of(address.begin(), address.end(), [](std::uint8_t byte) { return byte == 0xaa; })); + } +} + static void CheckInetPtonEx() { std::array address{}; Require(sceNetInetPtonEx(2, "192.168.1.20", address.data(), 0) == 1); @@ -755,10 +851,10 @@ static void CheckInetPtonEx() { Require(Failed(sceNetInetPtonEx(99, "127.0.0.1", address.data(), 0), 47)); Require(Failed(sceNetInetPtonEx(2, nullptr, address.data(), 0), 22)); Require(Failed(sceNetInetPtonEx(2, "127.0.0.1", nullptr, 0), 22)); - for (const int flags : {1, -1}) { - bool threw = false; - try { sceNetInetPtonEx(2, "127.0.0.1", address.data(), flags); } catch (const std::runtime_error&) { threw = true; } - Require(threw); + for (const int flags : {1, 0x100, -1}) { + address.fill(0); + Require(sceNetInetPtonEx(2, "1.2.3", address.data(), flags) == 1); + Require(address[0] == 1 && address[1] == 2 && address[2] == 0 && address[3] == 3); } } @@ -1035,7 +1131,7 @@ static void CheckUnspecifiedIpv6() { const char payload[] = "IPv6 wildcard receive"; const auto sent = sceNetSendto(sender, payload, sizeof(payload), 0, address.data(), address.size()); - if (sent < 0 && *sceNetErrnoLoc() == 51 && HostIpv6LoopbackUnavailable()) { + if (sent < 0 && (*sceNetErrnoLoc() == 49 || *sceNetErrnoLoc() == 51 || *sceNetErrnoLoc() == 65) && HostIpv6LoopbackUnavailable()) { Require(sceNetSocketClose(sender) == 0); Require(sceNetSocketClose(receiver) == 0); ipv6Unavailable = true; @@ -1178,6 +1274,112 @@ static void CheckCloseKernelSocket() { Require(Failed(sceNetSocketClose(kernel), 9)); } +static std::array RequireSockInfo(int socket, int type, int flags, const std::array& local, + const std::array& remote) { + std::array records; + records.fill(0xa5); + Require(sceNetGetSockInfo(socket, records.data(), 2, 0) == 1); + std::array expected{}; + std::memcpy(expected.data() + 0x24, &socket, sizeof(socket)); + expected[0x28] = static_cast(type); + std::memcpy(expected.data() + 0x34, local.data() + 4, 4); + std::memcpy(expected.data() + 0x38, remote.data() + 4, 4); + std::memcpy(expected.data() + 0x3c, local.data() + 2, 2); + std::memcpy(expected.data() + 0x3e, remote.data() + 2, 2); + std::memcpy(expected.data() + 0x48, &flags, sizeof(flags)); + for (const auto [offset, option] : {std::array{0x64, 0x1002}, std::array{0x68, 0x1001}}) { + int size = 0; + std::uint32_t length = sizeof(size); + Require(sceNetGetsockopt(socket, 0xffff, option, &size, &length) == 0 && size > 0); + std::memcpy(expected.data() + offset, &size, sizeof(size)); + } + Require(std::equal(expected.begin(), expected.end(), records.begin())); + Require(std::all_of(records.begin() + 0xa0, records.end(), [](auto byte) { return byte == 0xa5; })); + return expected; +} + +static std::vector RequireSockInfoList(int flags) { + const int count = sceNetGetSockInfo(-1, nullptr, 0, flags); + Require(count >= 0); + std::vector records(static_cast(count + 1) * 0xa0, 0xa5); + Require(sceNetGetSockInfo(-1, records.data(), count + 1, flags) == count); + Require(std::all_of(records.end() - 0xa0, records.end(), [](auto byte) { return byte == 0xa5; })); + std::vector sockets; + for (int index = 0; index < count; ++index) { + const auto listed = records.begin() + index * 0xa0; + int socket = 0; + std::memcpy(&socket, &listed[0x24], sizeof(socket)); + Require(sockets.empty() || socket > sockets.back()); + std::array single; + Require(sceNetGetSockInfo(socket, single.data(), 1, 0) == 1 && std::equal(single.begin(), single.end(), listed)); + sockets.push_back(socket); + } + const int one = std::min(count, 1); + std::array first; + first.fill(0xa5); + Require(sceNetGetSockInfo(-1, first.data(), 1, flags) == one && std::equal(first.begin(), first.begin() + one * 0xa0, records.begin())); + Require(std::all_of(first.begin() + one * 0xa0, first.end(), [](auto byte) { return byte == 0xa5; })); + return sockets; +} + +static void CheckSockInfo() { + const auto earlier = RequireSockInfoList(0); + const std::array unnamed{}; + const int datagram = sceNetSocket("sockinfo-datagram", 2, 2, 17); + Require(datagram >= 0); + RequireSockInfo(datagram, 2, 1, unnamed, unnamed); + std::array datagramAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(datagram, datagramAddress.data(), datagramAddress.size()) == 0); + std::uint32_t size = datagramAddress.size(); + Require(sceNetGetsockname(datagram, datagramAddress.data(), &size) == 0 && size == 16); + RequireSockInfo(datagram, 2, 1, datagramAddress, unnamed); + const int listener = sceNetSocket("sockinfo-listener", 2, 1, 6); + std::array listening{16, 2, 0, 0, 127, 0, 0, 1}; + Require(listener >= 0 && sceNetBind_nid_postfix(listener, listening.data(), listening.size()) == 0 && sceNetListen(listener, 1) == 0); + size = listening.size(); + Require(sceNetGetsockname(listener, listening.data(), &size) == 0 && size == 16); + RequireSockInfo(listener, 1, 1, listening, unnamed); + const int client = sceNetSocket("sockinfo-client", 2, 1, 6); + Require(client >= 0 && sceNetConnect(client, listening.data(), listening.size()) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0); + std::array clientAddress{}; + size = clientAddress.size(); + Require(sceNetGetsockname(client, clientAddress.data(), &size) == 0 && size == 16); + RequireSockInfo(client, 1, 1, clientAddress, listening); + RequireSockInfo(accepted, 1, 1, listening, clientAddress); + Require(sceNetConnect(datagram, listening.data(), listening.size()) == 0); + RequireSockInfo(datagram, 2, 1, datagramAddress, listening); + const int nonblocking = 1; + const std::array buffers{65536, 32768}; + Require(sceNetSetsockopt(datagram, 0xffff, 0x1200, &nonblocking, sizeof(nonblocking)) == 0); + Require(sceNetSetsockopt(datagram, 0xffff, 0x1002, &buffers[0], sizeof(buffers[0])) == 0); + Require(sceNetSetsockopt(datagram, 0xffff, 0x1001, &buffers[1], sizeof(buffers[1])) == 0); + const auto configured = RequireSockInfo(datagram, 2, 0x200001, datagramAddress, listening); + Require(std::memcmp(configured.data() + 0x64, buffers.data(), sizeof(buffers)) == 0); + std::array record; + record.fill(0xa5); + Require(Failed(sceNetGetSockInfo(datagram, nullptr, 1, 0), 22)); + Require(Failed(sceNetGetSockInfo(datagram, record.data(), 0, 0), 22)); + Require(Failed(sceNetGetSockInfo(-1, record.data(), 0, 0), 22)); + for (const int flags : {1, 0x41, 0x1000}) { + Require(Failed(sceNetGetSockInfo(datagram, record.data(), 1, flags), 22)); + Require(Failed(sceNetGetSockInfo(-1, nullptr, 0, flags), 22)); + } + const int inet6 = sceNetSocket("sockinfo-inet6", 28, 2, 17); + Require(inet6 >= 0 ? Failed(sceNetGetSockInfo(inet6, record.data(), 1, 0), 47) : *sceNetErrnoLoc() == 47); + Require(std::all_of(record.begin(), record.end(), [](auto byte) { return byte == 0xa5; })); + std::vector expected = earlier; + expected.insert(expected.end(), {datagram, listener, client, accepted}); + std::sort(expected.begin(), expected.end()); + Require(RequireSockInfoList(0) == expected && RequireSockInfoList(0x40) == expected); + Require(sceNetGetSockInfo(datagram, record.data(), 1, 0x40) == 1 && std::equal(configured.begin(), configured.end(), record.begin())); + if (inet6 >= 0) Require(sceNetSocketClose(inet6) == 0); + for (const int descriptor : {accepted, client, listener, datagram}) Require(sceNetSocketClose(descriptor) == 0); + Require(Failed(sceNetGetSockInfo(datagram, record.data(), 1, 0), 9)); + Require(RequireSockInfoList(0) == earlier); +} + static void CheckDatagramDisconnect() { const int peer = sceNetSocket("disconnect-peer", 2, 2, 17); const int other = sceNetSocket("disconnect-other", 2, 2, 17); @@ -1230,6 +1432,9 @@ int main() { Require(sceNetInit_nid_postfix() == 0); CheckEpollWritable(); CheckPoolStats(); + CheckIfName(); + CheckRandom(); + CheckNetMemory(); CheckNetstat(); CheckEmptyEpoll(); CheckDontWait(); @@ -1238,6 +1443,7 @@ int main() { CheckAddressText(28, "::1"); CheckAddressText(28, "1234:5678:9abc:def0:1234:5678:9abc:def0"); CheckInetPtonWhitespace(); + CheckInetPtonExParts(); CheckInetPtonEx(); CheckUsleep(); CheckSystemTime(); @@ -1258,6 +1464,7 @@ int main() { } CheckDatagramDisconnect(); CheckCloseKernelSocket(); + CheckSockInfo(); const int closed = sceNetSocket("closed-by-kernel", 2, 2, 17); Require(closed >= 0); @@ -1546,6 +1753,53 @@ int main() { } Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0); + std::array loopback6{}; + loopback6[15] = 1; + std::memset(reverse, 0x5A, sizeof(reverse)); + const int reverse6_result = sceNetResolverStartAton6(resolver, loopback6.data(), reverse, sizeof(reverse), 5000000, 1, 0); + Require(reverse6_result == 0 || reverse6_result == static_cast(0x804101E1)); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == reverse6_result); + char host_reverse6[256]{}; + sockaddr_in6 host_loopback6{}; + host_loopback6.sin6_family = AF_INET6; + std::memcpy(&host_loopback6.sin6_addr, loopback6.data(), loopback6.size()); + const bool host_named6 = ::getnameinfo(reinterpret_cast(&host_loopback6), sizeof(host_loopback6), + host_reverse6, sizeof(host_reverse6), nullptr, 0, NI_NAMEREQD) == 0; + Require(host_named6 == (reverse6_result == 0)); + if (reverse6_result == 0) { + Require(std::strcmp(reverse, host_reverse6) == 0); + const auto name_length = static_cast(std::strlen(host_reverse6)); + char exact[256]; + std::memset(exact, 0x5A, sizeof(exact)); + Require(sceNetResolverStartAton6(resolver, loopback6.data(), exact, name_length + 1, 5000000, 1, 0) == 0 && + std::strcmp(exact, host_reverse6) == 0 && static_cast(exact[name_length + 1]) == 0x5A); + std::memset(exact, 0x5A, sizeof(exact)); + Require(sceNetResolverStartAton6(resolver, loopback6.data(), exact, name_length, 5000000, 1, 0) == + static_cast(0x804101E1) && static_cast(exact[0]) == 0x5A); + } else { + Require(static_cast(reverse[0]) == 0x5A); + } + std::memset(reverse, 0x5A, sizeof(reverse)); + Require(sceNetResolverAbort(resolver, 2) == 0); + Require(sceNetResolverStartAton6(resolver, loopback6.data(), reverse, sizeof(reverse), 5000000, 1, 0) == + static_cast(0x80410104) && *sceNetErrnoLoc() == 4 && static_cast(reverse[0]) == 0x5A); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x80410104)); + Require(sceNetResolverAbort(resolver, 1) == 0); + Require(sceNetResolverStartAton6(resolver, loopback6.data(), reverse, sizeof(reverse), 5000000, 1, 0) == reverse6_result); + Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == static_cast(0x80410104)); + Require(sceNetResolverStartAton6(resolver, nullptr, reverse, sizeof(reverse), 5000000, 1, 0) == static_cast(0x80410116) && + *sceNetErrnoLoc() == 22); + Require(sceNetResolverStartAton6(resolver, loopback6.data(), nullptr, sizeof(reverse), 5000000, 1, 0) == static_cast(0x80410116)); + Require(sceNetResolverStartAton6(resolver, loopback6.data(), reverse, 0, 5000000, 1, 0) == static_cast(0x80410116)); + Require(sceNetResolverStartAton6(resolver + 1000, loopback6.data(), reverse, sizeof(reverse), 5000000, 1, 0) == + static_cast(0x80410109) && *sceNetErrnoLoc() == 9); + for (const int flags : {0x1, 0x10000, -1}) { + bool threw = false; + try { sceNetResolverStartAton6(resolver, loopback6.data(), reverse, sizeof(reverse), 5000000, 1, flags); } + catch (const std::runtime_error&) { threw = true; } + Require(threw); + } + const int other = sceNetResolverCreate("guest-sce-net-other", 0, 0); Require(other >= 0 && other != resolver); Require(sceNetResolverAbort(other, 3) == 0); @@ -1554,6 +1808,15 @@ int main() { Require(sceNetResolverAbort(other, 0) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9); Require(sceNetResolverAbort(-1, 1) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9); Require(sceNetResolverGetError(resolver, nullptr) == static_cast(0x80410116) && *sceNetErrnoLoc() == 22); + Require(sceNetClearDnsCache(0) == 0); + ipv4.fill(0xA5); + Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0 && ipv4[0] == 127); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); + for (const int flags : {0x1, -1}) { + bool threw = false; + try { sceNetClearDnsCache(flags); } catch (const std::runtime_error&) { threw = true; } + Require(threw); + } Require(sceNetResolverDestroy(resolver) == 0); resolver_error = -1; Require(sceNetResolverGetError(resolver, &resolver_error) == static_cast(0x80410109) && diff --git a/core/libs/tests/GuestSocketAddressLength.cpp b/core/libs/tests/GuestSocketAddressLength.cpp new file mode 100644 index 0000000000..930a8626d6 --- /dev/null +++ b/core/libs/tests/GuestSocketAddressLength.cpp @@ -0,0 +1,73 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int Stream = 1; +constexpr int Datagram = 2; +constexpr int ErrorFault = 14; +constexpr int ErrorInvalid = 22; +constexpr int ErrorNameTooLong = 63; + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + std::abort(); +} + +bool Fails(long result, int error) { return result == -1 && *__error_nid_postfix() == error; } + +std::array Loopback() { + std::array address{16, 2}; + address[4] = 127; + address[7] = 1; + return address; +} + +void ShortAddressesAreInvalid() { + const int stream = socket_nid_postfix(2, Stream, 0); + const int datagram = socket_nid_postfix(2, Datagram, 0); + Require(stream >= 0 && datagram >= 0, "socket failed"); + const auto address = Loopback(); + Require(Fails(connect_nid_postfix(stream, address.data(), 1), ErrorInvalid), "connect with a length below the size of sa_family must fail with EINVAL (22)"); + Require(Fails(bind_nid_postfix(stream, address.data(), 0), ErrorInvalid), "bind with length 0 must fail with EINVAL (22)"); + Require(Fails(connect_nid_postfix(stream, address.data(), 3), ErrorInvalid), "connect with an AF_INET address shorter than sockaddr_in must fail with EINVAL (22)"); + Require(Fails(bind_nid_postfix(stream, address.data(), 15), ErrorInvalid), "bind with an AF_INET address shorter than sockaddr_in must fail with EINVAL (22)"); + Require(Fails(sendto_nid_postfix(datagram, "x", 1, 0, address.data(), 8), ErrorInvalid), "sendto with an AF_INET address shorter than sockaddr_in must fail with EINVAL (22)"); + std::array v6{28, 28}; + const int stream6 = socket_nid_postfix(28, Stream, 0); + Require(stream6 >= 0, "socket failed"); + Require(Fails(connect_nid_postfix(stream6, v6.data(), 20), ErrorInvalid), "connect with an AF_INET6 address shorter than sockaddr_in6 must fail with EINVAL (22)"); + Require(Fails(bind_nid_postfix(stream, address.data(), 256), ErrorNameTooLong), "an address longer than SOCK_MAXADDRLEN (255) must fail with ENAMETOOLONG (63)"); + Require(Fails(bind_nid_postfix(stream, nullptr, address.size()), ErrorFault), "a null address must fail with EFAULT (14)"); + Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0 && close_nid_postfix(stream6) == 0, "close failed"); +} + +void FullAddressesStillWork() { + const int stream = socket_nid_postfix(2, Stream, 0); + Require(stream >= 0, "socket failed"); + const auto address = Loopback(); + Require(bind_nid_postfix(stream, address.data(), address.size()) == 0, "bind of a full sockaddr_in failed"); + Require(close_nid_postfix(stream) == 0, "close failed"); +} + +} + +int main() { + ShortAddressesAreInvalid(); + FullAddressesStillWork(); + return 0; +} diff --git a/core/libs/tests/GuestSocketConnectListening.cpp b/core/libs/tests/GuestSocketConnectListening.cpp index 9bfffa3172..0f42c12bdb 100644 --- a/core/libs/tests/GuestSocketConnectListening.cpp +++ b/core/libs/tests/GuestSocketConnectListening.cpp @@ -20,6 +20,7 @@ namespace { constexpr int Inet = 2; constexpr int Stream = 1; constexpr int ErrorNotSupported = 45; +constexpr int ErrorInvalid = 22; void Require(bool condition, const char* what) { if (condition) return; @@ -57,6 +58,12 @@ int main() { Require(connect_nid_postfix(listener, address.data(), 1) == -1 && *__error_nid_postfix() == shortError, "connect on a listening socket with a 1-byte address must fail as on any socket, not with EOPNOTSUPP"); Require(nullError != ErrorNotSupported && shortError != ErrorNotSupported, "address errors must not be EOPNOTSUPP"); + const int connected = socket_nid_postfix(Inet, Stream, 0); + Require(connected >= 0 && connect_nid_postfix(connected, address.data(), address.size()) == 0, "connect to the listener failed"); + *__error_nid_postfix() = 0; + Require(listen_nid_postfix(connected, 1) == -1 && *__error_nid_postfix() == ErrorInvalid, + "listen on a connected socket must fail with EINVAL (22)"); + Require(close_nid_postfix(connected) == 0, "close failed"); Require(close_nid_postfix(plain) == 0, "close failed"); std::array wrongFamily{28, 28}; *__error_nid_postfix() = 0; diff --git a/core/libs/tests/GuestSocketEvents.cpp b/core/libs/tests/GuestSocketEvents.cpp index 7bda0e3e06..1ef4eea859 100644 --- a/core/libs/tests/GuestSocketEvents.cpp +++ b/core/libs/tests/GuestSocketEvents.cpp @@ -16,6 +16,9 @@ int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI pipe_nid_postfix(int*); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); int APS5_VABI sceKernelWaitEqueue(KernelEqueue eq, KernelEvent* ev, int num, int* out, const KernelUseconds* timo); @@ -44,7 +47,57 @@ static bool Rejects(int (APS5_VABI *add)(KernelEqueue, int, std::size_t, void*), return false; } +static void PipeReadEvents() { + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(&eq, "pipe") == SCE_OK); + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + const KernelUseconds shortTimeout = 20000; + const KernelUseconds timeout = 1000000; + int marker = 0; + int count = 0; + std::array events{}; + + Require(sceKernelAddReadEvent(eq, pipe[0], 64, &marker) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &shortTimeout) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(count == 0); + Require(write_nid_postfix(pipe[1], "abc", 3) == 3); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK); + Require(count == 1 && events[0].ident == static_cast(pipe[0]) && events[0].filter == EVFILT_READ); + Require(events[0].data == 3 && events[0].udata == &marker && (events[0].flags & EV_EOF) == 0); + char buffer[4]{}; + Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 3); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &shortTimeout) == SCE_KERNEL_ERROR_ETIMEDOUT); + + const auto start = std::chrono::steady_clock::now(); + std::thread writer([&pipe] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(write_nid_postfix(pipe[1], "d", 1) == 1); + }); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, nullptr) == SCE_OK); + writer.join(); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); + Require(count == 1 && events[0].data == 1); + + Require(close_nid_postfix(pipe[1]) == 0); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK); + Require(count == 1 && events[0].data == 1 && (events[0].flags & EV_EOF) != 0); + Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 1); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK); + Require(count == 1 && events[0].data == 0 && (events[0].flags & EV_EOF) != 0); + Require(sceKernelDeleteReadEvent(eq, pipe[0]) == SCE_OK); + Require(sceKernelDeleteReadEvent(eq, pipe[0]) == SCE_KERNEL_ERROR_ENOENT); + + int oneWay[2]; + Require(pipe_nid_postfix(oneWay) == 0); + Require(Rejects(sceKernelAddWriteEvent, eq, oneWay[1], 1)); + Require(close_nid_postfix(oneWay[0]) == 0 && close_nid_postfix(oneWay[1]) == 0); + Require(close_nid_postfix(pipe[0]) == 0); + Require(sceKernelDeleteEqueue(eq) == SCE_OK); +} + int main() { + PipeReadEvents(); const int listener = socket_nid_postfix(2, 1, 6); Require(listener >= 0); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; diff --git a/core/libs/tests/GuestSocketOnNonSocket.cpp b/core/libs/tests/GuestSocketOnNonSocket.cpp new file mode 100644 index 0000000000..953bd473d0 --- /dev/null +++ b/core/libs/tests/GuestSocketOnNonSocket.cpp @@ -0,0 +1,73 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI listen_nid_postfix(int, int); +int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI shutdown_nid_postfix(int, int); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +int APS5_VABI getsockopt_nid_postfix(int, int, int, void*, std::uint32_t*); +std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int ErrorBadDescriptor = 9; +constexpr int ErrorNotSocket = 38; + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + std::abort(); +} + +bool Fails(long result, int error) { return result == -1 && *__error_nid_postfix() == error; } + +void CallsOnAnOpenPipeAreNotSocketCalls() { + int ends[2]; + Require(pipe_nid_postfix(ends) == 0, "pipe failed"); + std::array address{16, 2}; + std::uint32_t size = address.size(); + char buffer[4]; + int option = 0; + std::uint32_t optionSize = sizeof(option); + Require(Fails(bind_nid_postfix(ends[0], address.data(), address.size()), ErrorNotSocket), "bind on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(listen_nid_postfix(ends[0], 1), ErrorNotSocket), "listen on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(accept_nid_postfix(ends[0], nullptr, nullptr), ErrorNotSocket), "accept on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(connect_nid_postfix(ends[0], address.data(), address.size()), ErrorNotSocket), "connect on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(shutdown_nid_postfix(ends[0], 1), ErrorNotSocket), "shutdown on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(getsockname_nid_postfix(ends[0], address.data(), &size), ErrorNotSocket), "getsockname on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(setsockopt_nid_postfix(ends[0], 0xffff, 4, &option, sizeof(option)), ErrorNotSocket), "setsockopt on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(getsockopt_nid_postfix(ends[0], 0xffff, 4, &option, &optionSize), ErrorNotSocket), "getsockopt on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(send_nid_postfix(ends[1], "x", 1, 0), ErrorNotSocket), "send on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(recv_nid_postfix(ends[0], buffer, sizeof(buffer), 0), ErrorNotSocket), "recv on a pipe must fail with ENOTSOCK (38)"); + Require(close_nid_postfix(ends[0]) == 0 && close_nid_postfix(ends[1]) == 0, "close failed"); +} + +void ClosedAndInvalidDescriptorsAreBad() { + int ends[2]; + Require(pipe_nid_postfix(ends) == 0, "pipe failed"); + Require(close_nid_postfix(ends[0]) == 0 && close_nid_postfix(ends[1]) == 0, "close failed"); + std::array address{16, 2}; + Require(Fails(bind_nid_postfix(ends[0], address.data(), address.size()), ErrorBadDescriptor), "bind on a closed descriptor must fail with EBADF (9)"); + Require(Fails(listen_nid_postfix(-1, 1), ErrorBadDescriptor), "listen on -1 must fail with EBADF (9)"); + Require(Fails(connect_nid_postfix(1 << 20, address.data(), address.size()), ErrorBadDescriptor), "connect on a descriptor that was never open must fail with EBADF (9)"); +} + +} + +int main() { + CallsOnAnOpenPipeAreNotSocketCalls(); + ClosedAndInvalidDescriptorsAreBad(); + return 0; +} diff --git a/core/libs/tests/GuestSocketPair.cpp b/core/libs/tests/GuestSocketPair.cpp index 51d909e85d..9a40c92040 100644 --- a/core/libs/tests/GuestSocketPair.cpp +++ b/core/libs/tests/GuestSocketPair.cpp @@ -192,6 +192,7 @@ int main(int argc, char** argv) { Require(std::strcmp(received, ping) == 0); Require(fcntl_nid_postfix(pair[1], 3) == 2); + Require(fcntl_nid_postfix(pair[0], 1) == 0 && fcntl_nid_postfix(pair[1], 1) == 0); Require(fcntl_nid_postfix(pair[1], 4, 4) == 0); Require(fcntl_nid_postfix(pair[1], 3) == 6); Require(recv_nid_postfix(pair[1], received, sizeof(received), 0) == -1 && *__error_nid_postfix() == 35); @@ -209,6 +210,7 @@ int main(int argc, char** argv) { Require(socketpair_nid_postfix(Unix, Stream | NonBlocking | CloseOnExec, 0, pair) == 0); Require(fcntl_nid_postfix(pair[0], 3) == 6 && fcntl_nid_postfix(pair[1], 3) == 6); + Require(fcntl_nid_postfix(pair[0], 1) == 1 && fcntl_nid_postfix(pair[1], 1) == 1); Require(recv_nid_postfix(pair[0], received, sizeof(received), 0) == -1 && *__error_nid_postfix() == 35); Require(Exchange(pair[1], pair[0], "wake up")); Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); diff --git a/core/libs/tests/GuestSocketTimeouts.cpp b/core/libs/tests/GuestSocketTimeouts.cpp index b475463949..2ff28a2173 100644 --- a/core/libs/tests/GuestSocketTimeouts.cpp +++ b/core/libs/tests/GuestSocketTimeouts.cpp @@ -119,13 +119,11 @@ int main() { Msghdr message{nullptr, 0, &part, 1, nullptr, 0, 0}; Require(Fails(recvmsg_nid_postfix(datagram, &message, 0), WouldBlock)); Require(Fails(read_nid_postfix(datagram, &byte, sizeof(byte)), WouldBlock)); -#ifndef _WIN32 Require(Set(datagram, ReceiveTimeout, 60, 0) == 0); Require(Fails(recv_nid_postfix(datagram, &byte, sizeof(byte), DontWait), WouldBlock)); Require(Fails(recvfrom_nid_postfix(datagram, &byte, sizeof(byte), DontWait, nullptr, nullptr), WouldBlock)); Require(Fails(recvmsg_nid_postfix(datagram, &message, DontWait), WouldBlock)); Require(Set(datagram, ReceiveTimeout, 0, 60000) == 0); -#endif Require(sendto_nid_postfix(datagram, "d", 1, 0, own.data(), own.size()) == 1); Require(recv_nid_postfix(datagram, &byte, sizeof(byte), 0) == 1 && byte == 'd'); diff --git a/core/libs/tests/GuestSsl.cpp b/core/libs/tests/GuestSsl.cpp index efda5060a0..4becc68d8c 100644 --- a/core/libs/tests/GuestSsl.cpp +++ b/core/libs/tests/GuestSsl.cpp @@ -18,6 +18,7 @@ int APS5_VABI sceSslSetVerifyCallback(int, int (APS5_VABI *)(int, unsigned int, int APS5_VABI sceSslConnect(int); int APS5_VABI sceSslRead(int, void*, std::size_t, int); int APS5_VABI sceSslWrite(int, void*, std::size_t, int); +int APS5_VABI sceSslClose(int); int APS5_VABI sceSslEnableVerifyOption(int, std::uint32_t); int APS5_VABI sceSslDisableVerifyOption(int, std::uint32_t); int APS5_VABI sceSslSetMinSslVersion(int, int); @@ -101,7 +102,11 @@ int main() { Require(list.certs == nullptr && list.num == 0); constexpr int invalidId = static_cast(0x8095F006); - Require(Throws([&] { sceSslCreateConnection(context, 0, nullptr); })); + constexpr int invalidValue = static_cast(0x8095F007); + constexpr int badLength = static_cast(0x80951773); + constexpr int badDescriptor = static_cast(0x80410109); + Require(sceSslInit_nid_postfix(0) == static_cast(0x80020016)); + Require(sceSslCreateConnection(context, 0, nullptr) == invalidValue); Require(sceSslCreateConnection(context + 1000, 0, "example.com") == invalidId); const int connection = sceSslCreateConnection(context, 0, "example.com"); @@ -113,39 +118,51 @@ int main() { Require(sceSslEnableVerifyOption(connection, serverVerify | cnCheck) == 0); Require(sceSslDisableVerifyOption(connection, cnCheck) == 0); Require(sceSslDisableVerifyOption(connection, serverVerify | cnCheck) == 0); - Require(sceSslSetMinSslVersion(connection, 0) == 0); + Require(sceSslEnableVerifyOption(connection, 0x40) == invalidValue); + Require(sceSslDisableVerifyOption(connection, 0x1000) == invalidValue); + Require(sceSslSetMinSslVersion(connection, 0) == invalidValue); + Require(sceSslSetMinSslVersion(connection, 1) == invalidValue); + Require(sceSslSetMinSslVersion(connection, 2) == 0); Require(sceSslSetMinSslVersion(connection, 5) == 0); - Require(Throws([&] { sceSslSetMinSslVersion(connection, -1); })); - Require(Throws([&] { sceSslSetMinSslVersion(connection, 6); })); + Require(sceSslSetMinSslVersion(connection, -1) == invalidValue); + Require(sceSslSetMinSslVersion(connection, 6) == invalidValue); Require(sceSslGetAlpnSelected(connection) == nullptr); Require(sceSslSetAlpn(connection, "http/1.1") == 0); - Require(sceSslSetAlpn(connection, "") == 0); + Require(sceSslSetAlpn(connection, "") == badLength); Require(Throws([&] { sceSslSetAlpn(connection, nullptr); })); Require(sceSslGetAlpnSelected(connection) == nullptr); char buffer[4]{}; - Require(Throws([&] { sceSslConnect(connection); })); - Require(Throws([&] { sceSslRead(connection, buffer, sizeof(buffer), 0); })); - Require(Throws([&] { sceSslWrite(connection, buffer, sizeof(buffer), 0); })); + Require(sceSslConnect(connection) == badDescriptor); + Require(sceSslRead(connection, buffer, sizeof(buffer), 0) == badDescriptor); + Require(sceSslWrite(connection, buffer, sizeof(buffer), 0) == badDescriptor); + Require(sceSslWrite(connection, buffer, sizeof(buffer), 1) == invalidValue); + Require(sceSslClose(connection) == 0); const int unknown = connection + 1000; - Require(Throws([&] { sceSslSetVerifyCallback(unknown, nullptr, nullptr); })); - Require(Throws([&] { sceSslEnableVerifyOption(unknown, serverVerify); })); - Require(Throws([&] { sceSslDisableVerifyOption(unknown, serverVerify); })); - Require(Throws([&] { sceSslSetMinSslVersion(unknown, 5); })); - Require(Throws([&] { sceSslSetAlpn(unknown, "http/1.1"); })); + Require(sceSslSetVerifyCallback(unknown, nullptr, nullptr) == invalidId); + Require(sceSslEnableVerifyOption(unknown, serverVerify) == invalidId); + Require(sceSslDisableVerifyOption(unknown, serverVerify) == invalidId); + Require(sceSslSetMinSslVersion(unknown, 5) == invalidId); + Require(sceSslSetAlpn(unknown, "http/1.1") == invalidId); Require(Throws([&] { sceSslGetAlpnSelected(unknown); })); - Require(Throws([&] { sceSslDeleteConnection(unknown); })); + Require(sceSslConnect(unknown) == invalidId); + Require(sceSslRead(unknown, buffer, sizeof(buffer), 0) == invalidId); + Require(sceSslWrite(unknown, buffer, sizeof(buffer), 0) == invalidId); + Require(sceSslClose(unknown) == invalidId); + Require(sceSslDeleteConnection(unknown) == invalidId); Require(sceSslTerm_nid_postfix(context) == 0); - Require(Throws([&] { sceSslSetVerifyCallback(connection, nullptr, nullptr); })); - Require(Throws([&] { sceSslEnableVerifyOption(connection, serverVerify); })); - Require(Throws([&] { sceSslDisableVerifyOption(connection, serverVerify); })); - Require(Throws([&] { sceSslSetMinSslVersion(connection, 5); })); - Require(Throws([&] { sceSslSetAlpn(connection, "http/1.1"); })); + Require(sceSslTerm_nid_postfix(context) == invalidId); + Require(sceSslSetVerifyCallback(connection, nullptr, nullptr) == invalidId); + Require(sceSslEnableVerifyOption(connection, serverVerify) == invalidId); + Require(sceSslDisableVerifyOption(connection, serverVerify) == invalidId); + Require(sceSslSetMinSslVersion(connection, 5) == invalidId); + Require(sceSslSetAlpn(connection, "http/1.1") == invalidId); Require(Throws([&] { sceSslGetAlpnSelected(connection); })); + Require(sceSslConnect(connection) == invalidId); + Require(sceSslClose(connection) == invalidId); Require(sceSslCreateConnection(context, 0, "example.com") == invalidId); - Require(sceSslDeleteConnection(connection) == 0); - Require(Throws([&] { sceSslDeleteConnection(connection); })); + Require(sceSslDeleteConnection(connection) == invalidId); Require(sceSslGetCaList(context, nullptr) == invalidArg); Require(sceSslFreeCaList(context, nullptr) == invalidArg); diff --git a/core/libs/tests/GuestSslLoad.cpp b/core/libs/tests/GuestSslLoad.cpp index 95e12521f7..28b894ddb4 100644 --- a/core/libs/tests/GuestSslLoad.cpp +++ b/core/libs/tests/GuestSslLoad.cpp @@ -39,6 +39,7 @@ int APS5_VABI sceSslGetPem(int, void*, char*, std::size_t*); namespace { constexpr int NotFound = static_cast(0x8095F004); +constexpr int InvalidId = static_cast(0x8095F006); void Check(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); @@ -120,8 +121,8 @@ void ConnectionLifetime() { Check(sceSslSetVerifyCallback(retained, nullptr, nullptr) == 0, "credential unload invalidated the connection"); Check(sceSslFreeCaCerts(id, &bytes) == 0 && Stats(id).currentInuseSize == 0, "CA snapshot leaked with a retained connection"); Check(sceSslTerm_nid_postfix(id) == 0, "context termination failed with a retained connection"); - Throws([&] { sceSslSetVerifyCallback(retained, nullptr, nullptr); }); - Check(sceSslDeleteConnection(retained) == 0, "terminated context's connection cannot be released"); + Check(sceSslSetVerifyCallback(retained, nullptr, nullptr) == InvalidId, "terminated context's connection did not report an invalid id"); + Check(sceSslDeleteConnection(retained) == InvalidId, "terminated context's connection was still deletable"); Check(sceSslGetCaCerts(other, &empty) == NotFound, "unrelated context was invalidated"); Check(sceSslTerm_nid_postfix(other) == 0, "unrelated context termination failed"); } @@ -188,7 +189,7 @@ void CertificateReaders() { unsigned char serialData[8]{}; std::size_t serialLen = sizeof(serialData); Check(sceSslGetSerialNumber(id, cert, serialData, &serialLen) == 0, "get serial failed"); - Check(serialLen == 3, "serial length mismatch"); + Check(serialLen == sizeof(serialData) && serialData[3] == 0, "a large enough serial buffer changed the caller's length"); Check(serialData[0] == 0x00 && serialData[1] == 0x80 && serialData[2] == 0xa1, "serial bytes mismatch"); Check(sceSslGetSerialNumber(id, cert, serialData, nullptr) == static_cast(0x8095177a), "null serial length accepted"); Throws([&] { sceSslGetSerialNumber(id, nullptr, serialData, &serialLen); }); @@ -217,11 +218,11 @@ void CertificateReaders() { char val[64]{}; std::size_t vlen = 0; Check(sceSslGetNameEntryInfo(id, subject, 0, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 0 info failed"); - Check(std::strcmp(oid, "C") == 0 && std::strcmp(val, "GB") == 0 && vlen == 2, "entry 0 mismatch"); + Check(std::strcmp(oid, "countryName") == 0 && std::strcmp(val, "GB") == 0 && vlen == 2, "entry 0 mismatch"); Check(sceSslGetNameEntryInfo(id, subject, 1, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 1 info failed"); - Check(std::strcmp(oid, "O") == 0 && std::strcmp(val, "AnyPS5 Tests") == 0 && vlen == 12, "entry 1 mismatch"); + Check(std::strcmp(oid, "organizationName") == 0 && std::strcmp(val, "AnyPS5 Tests") == 0 && vlen == 12, "entry 1 mismatch"); Check(sceSslGetNameEntryInfo(id, subject, 2, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 2 info failed"); - Check(std::strcmp(oid, "CN") == 0 && std::strcmp(val, "AnyPS5 fixture root") == 0 && vlen == 19, "entry 2 mismatch"); + Check(std::strcmp(oid, "commonName") == 0 && std::strcmp(val, "AnyPS5 fixture root") == 0 && vlen == 19, "entry 2 mismatch"); char smallVal[4]{}; std::size_t fullLen = 0; @@ -296,7 +297,7 @@ void CertificateReaders() { Throws([&] { sceSslGetPem(id, nullptr, pemBuf.data(), &writtenLen); }); Throws([&] { sceSslGetPem(id, fake, pemBuf.data(), &writtenLen); }); - Throws([&] { sceSslClose(1); }); + Check(sceSslClose(1) == InvalidId, "closing an unknown connection did not report an invalid id"); Check(sceSslFreeCaList(id, &handles) == 0, "free ca list failed"); Check(sceSslTerm_nid_postfix(id) == 0, "term failed"); diff --git a/core/libs/tests/GuestSslPs5Readers.cpp b/core/libs/tests/GuestSslPs5Readers.cpp index 7f7bf6e909..caffe1cf3e 100644 --- a/core/libs/tests/GuestSslPs5Readers.cpp +++ b/core/libs/tests/GuestSslPs5Readers.cpp @@ -66,10 +66,10 @@ void Readers() { Throws([&] { sceSslGetSubjectName(other, certificate); }); Throws([&] { sceSslGetNameEntryCount(other, subject); }); Throws([&] { sceSslFreeSslCertName(other, subject); }); - std::array oid{}, value{}; + std::array oid{}, value{}; length = SIZE_MAX; Check(sceSslGetNameEntryInfo(context, subject, 1, oid.data(), oid.size(), value.data(), value.size(), &length) == 0, "PS5 eight-argument name query failed"); - Check(std::strcmp(oid.data(), "O") == 0 && std::strcmp(value.data(), "AnyPS5 Tests") == 0 && length == 12, "name output or full-width length changed"); + Check(std::strcmp(oid.data(), "organizationName") == 0 && std::strcmp(value.data(), "AnyPS5 Tests") == 0 && length == 12, "name output or full-width length changed"); length = SIZE_MAX; Check(sceSslGetPem(context, certificate, nullptr, &length) == 0 && length == std::strlen(RootPem) + 1, "PS5 PEM size query failed"); diff --git a/core/libs/tests/GuestSslTransport.cpp b/core/libs/tests/GuestSslTransport.cpp index 9b5bb60bc5..93fcb1b901 100644 --- a/core/libs/tests/GuestSslTransport.cpp +++ b/core/libs/tests/GuestSslTransport.cpp @@ -63,14 +63,22 @@ int APS5_VABI sceNetSocketClose(int); namespace { constexpr int ParallelConnections = 4; +constexpr int ErrorInvalidId = static_cast(0x8095f006); +constexpr int ErrorInvalidValue = static_cast(0x8095f007); constexpr int ErrorCnCheck = static_cast(0x8095f00b); constexpr int ErrorUnknownCa = static_cast(0x8095f00c); +constexpr int ErrorNotAfterCheck = static_cast(0x8095f00d); +constexpr int ErrorNotBeforeCheck = static_cast(0x8095f00e); +constexpr int ErrorFatalAlert = static_cast(0x8095f010); constexpr int ErrorWantPollIn = static_cast(0x8095f013); constexpr int ErrorWantPollOut = static_cast(0x8095f014); constexpr int ErrorCallbackCheck = static_cast(0x8095f0fd); constexpr int ErrorSocketClosed = static_cast(0x8095170d); -constexpr int ErrorReadTimeout = static_cast(0x80951710); -constexpr std::uint32_t ServerVerify = 0x1, CnCheck = 0x4; +constexpr int ErrorNetBadDescriptor = static_cast(0x80410109); +constexpr int ErrorNetPipe = static_cast(0x80410120); +constexpr int ErrorNetReset = static_cast(0x80410136); +constexpr int ErrorBadLength = static_cast(0x80951773); +constexpr std::uint32_t ServerVerify = 0x1, CnCheck = 0x4, NotAfterCheck = 0x8, NotBeforeCheck = 0x10, KnownCaCheck = 0x20; constexpr int VersionTls12 = 5; constexpr int GuestSocketLevel = 0xffff; constexpr int GuestNonBlocking = 0x1200; @@ -78,7 +86,8 @@ constexpr int GuestReceiveTimeout = 0x1006; constexpr int WaitRounds = 2000; const char* ServerProtocols[] = {"http/1.1", nullptr}; -enum class Serve { Close, Exchange, Drop, Reset }; +enum class Serve { Close, Exchange, Drop, Reset, Alert }; +enum class Leaf { Valid, Expired, Future }; struct GuestTimeval { std::int64_t seconds; @@ -106,18 +115,20 @@ void Settle() { std::this_thread::sleep_for(std::chrono::milliseconds(5)); } struct CallbackRecord { int context; int result; - int connection = 0; + int id = 0; unsigned int error = 0; int count = 0; + int calls = 0; void* leaf = nullptr; std::string commonName; }; -int APS5_VABI RecordingCallback(int connection, unsigned int error, void** certificates, int count, void* argument) { +int APS5_VABI RecordingCallback(int id, unsigned int error, void** certificates, int count, void* argument) { auto& record = *static_cast(argument); - record.connection = connection; + record.id = id; record.error = error; record.count = count; + ++record.calls; if (count > 0 && certificates && certificates[0]) { record.leaf = certificates[0]; void* name = sceSslGetSubjectName(record.context, record.leaf); @@ -140,9 +151,13 @@ struct Server { mbedtls_entropy_context entropy; mbedtls_ctr_drbg_context drbg; mbedtls_x509_crt certificate; + mbedtls_x509_crt expiredCertificate; + mbedtls_x509_crt futureCertificate; mbedtls_x509_crt authority; mbedtls_pk_context key; - mbedtls_ssl_config config; + mbedtls_pk_context expiredKey; + mbedtls_pk_context futureKey; + mbedtls_ssl_config configs[3]; std::uint16_t port = 0; std::string failure; @@ -151,9 +166,13 @@ struct Server { mbedtls_entropy_init(&entropy); mbedtls_ctr_drbg_init(&drbg); mbedtls_x509_crt_init(&certificate); + mbedtls_x509_crt_init(&expiredCertificate); + mbedtls_x509_crt_init(&futureCertificate); mbedtls_x509_crt_init(&authority); mbedtls_pk_init(&key); - mbedtls_ssl_config_init(&config); + mbedtls_pk_init(&expiredKey); + mbedtls_pk_init(&futureKey); + for (auto& config : configs) mbedtls_ssl_config_init(&config); Require(mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy, nullptr, 0) == 0, "seed server random generator"); Require(mbedtls_x509_crt_parse(&certificate, reinterpret_cast(ServerPem), sizeof(ServerPem)) == 0, "parse server certificate"); @@ -161,13 +180,26 @@ struct Server { "parse client authority"); Require(mbedtls_pk_parse_key(&key, reinterpret_cast(ServerKey), sizeof(ServerKey), nullptr, 0, mbedtls_ctr_drbg_random, &drbg) == 0, "parse server key"); - Require(mbedtls_ssl_config_defaults(&config, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT) == 0, - "configure server"); - mbedtls_ssl_conf_rng(&config, mbedtls_ctr_drbg_random, &drbg); - Require(mbedtls_ssl_conf_own_cert(&config, &certificate, &key) == 0, "set server certificate"); - mbedtls_ssl_conf_authmode(&config, MBEDTLS_SSL_VERIFY_REQUIRED); - mbedtls_ssl_conf_ca_chain(&config, &authority, nullptr); - Require(mbedtls_ssl_conf_alpn_protocols(&config, ServerProtocols) == 0, "set server protocols"); + Require(mbedtls_x509_crt_parse(&expiredCertificate, reinterpret_cast(ExpiredServerPem), sizeof(ExpiredServerPem)) == 0, + "parse expired server certificate"); + Require(mbedtls_pk_parse_key(&expiredKey, reinterpret_cast(ExpiredServerKey), sizeof(ExpiredServerKey), nullptr, 0, + mbedtls_ctr_drbg_random, &drbg) == 0, "parse expired server key"); + Require(mbedtls_x509_crt_parse(&futureCertificate, reinterpret_cast(FutureServerPem), sizeof(FutureServerPem)) == 0, + "parse future server certificate"); + Require(mbedtls_pk_parse_key(&futureKey, reinterpret_cast(FutureServerKey), sizeof(FutureServerKey), nullptr, 0, + mbedtls_ctr_drbg_random, &drbg) == 0, "parse future server key"); + mbedtls_x509_crt* leaves[] = {&certificate, &expiredCertificate, &futureCertificate}; + mbedtls_pk_context* keys[] = {&key, &expiredKey, &futureKey}; + for (int index = 0; index < 3; ++index) { + auto& config = configs[index]; + Require(mbedtls_ssl_config_defaults(&config, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT) == 0, + "configure server"); + mbedtls_ssl_conf_rng(&config, mbedtls_ctr_drbg_random, &drbg); + Require(mbedtls_ssl_conf_own_cert(&config, leaves[index], keys[index]) == 0, "set server certificate"); + mbedtls_ssl_conf_authmode(&config, MBEDTLS_SSL_VERIFY_REQUIRED); + mbedtls_ssl_conf_ca_chain(&config, &authority, nullptr); + Require(mbedtls_ssl_conf_alpn_protocols(&config, ServerProtocols) == 0, "set server protocols"); + } Require(mbedtls_net_bind(&listener, "127.0.0.1", "0", MBEDTLS_NET_PROTO_TCP) == 0, "bind loopback listener"); sockaddr_in address{}; #ifdef _WIN32 @@ -180,16 +212,21 @@ struct Server { } ~Server() { - mbedtls_ssl_config_free(&config); + for (auto& config : configs) mbedtls_ssl_config_free(&config); + mbedtls_pk_free(&futureKey); + mbedtls_pk_free(&expiredKey); mbedtls_pk_free(&key); mbedtls_x509_crt_free(&authority); + mbedtls_x509_crt_free(&futureCertificate); + mbedtls_x509_crt_free(&expiredCertificate); mbedtls_x509_crt_free(&certificate); mbedtls_ctr_drbg_free(&drbg); mbedtls_entropy_free(&entropy); mbedtls_net_free(&listener); } - void Serve(Serve mode, bool requireClientCertificate = true) { + void Serve(Serve mode, bool requireClientCertificate = true, Leaf leaf = Leaf::Valid) { + auto& config = configs[static_cast(leaf)]; mbedtls_ssl_conf_authmode(&config, requireClientCertificate ? MBEDTLS_SSL_VERIFY_REQUIRED : MBEDTLS_SSL_VERIFY_NONE); mbedtls_net_context client; mbedtls_net_init(&client); @@ -197,6 +234,8 @@ struct Server { mbedtls_ssl_init(&ssl); if (mbedtls_net_accept(&listener, &client, nullptr, 0, nullptr) != 0 || mbedtls_ssl_setup(&ssl, &config) != 0) { failure = "server accept or setup failed"; + } else if (mode == Serve::Alert) { + Alert(client); } else { mbedtls_ssl_set_bio(&ssl, &client, mbedtls_net_send, mbedtls_net_recv, nullptr); const int handshake = mbedtls_ssl_handshake(&ssl); @@ -209,6 +248,16 @@ struct Server { mbedtls_net_free(&client); } + void Alert(mbedtls_net_context& client) { + std::array hello{}; + if (mbedtls_net_recv(&client, hello.data(), hello.size()) <= 0) { + failure = "server did not receive the client hello before the alert"; + return; + } + const unsigned char alert[] = {0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x28}; + if (mbedtls_net_send(&client, alert, sizeof(alert)) != sizeof(alert)) failure = "server could not send the alert"; + } + void Reset(mbedtls_ssl_context& ssl, mbedtls_net_context& client) { unsigned char signal = 0; if (mbedtls_ssl_read(&ssl, &signal, 1) != 1) { @@ -291,14 +340,21 @@ int main(int argc, char** argv) { server.Serve(Serve::Close); server.Serve(Serve::Close); server.Serve(Serve::Close); + server.Serve(Serve::Close); server.Serve(Serve::Exchange); server.Serve(Serve::Exchange); server.Serve(Serve::Drop); server.Serve(Serve::Reset); + server.Serve(Serve::Alert); + server.Serve(Serve::Close, true, Leaf::Expired); + server.Serve(Serve::Close, true, Leaf::Expired); + server.Serve(Serve::Close, true, Leaf::Future); + server.Serve(Serve::Close, true, Leaf::Future); if (sockets.net) server.Serve(Serve::Exchange); for (int index = 0; index < ParallelConnections; ++index) server.Serve(Serve::Exchange); }); + Require(sceSslInit_nid_postfix(0) == static_cast(0x80020016), "an empty pool was accepted"); const int empty = sceSslInit_nid_postfix(256 * 1024); const int untrustedSocket = sockets.Open(server.port); const int untrusted = sceSslCreateConnection(empty, untrustedSocket, "localhost"); @@ -308,6 +364,12 @@ int main(int argc, char** argv) { Require(Throws([&] { sceSslRead(untrusted, scratch.data(), 1, 0); }), "a connection that failed verification was readable"); Require(sceSslClose(untrusted) == 0, "close a connection that failed verification"); Require(sceSslDeleteConnection(untrusted) == 0 && sockets.Close(untrustedSocket) == 0, "release the untrusted connection"); + Require(sceSslDeleteConnection(untrusted) == ErrorInvalidId && sceSslClose(untrusted) == ErrorInvalidId + && sceSslConnect(untrusted) == ErrorInvalidId && sceSslRead(untrusted, scratch.data(), 1, 0) == ErrorInvalidId + && sceSslWrite(untrusted, scratch.data(), 1, 0) == ErrorInvalidId && sceSslSetAlpn(untrusted, "h2") == ErrorInvalidId + && sceSslEnableVerifyOption(untrusted, CnCheck) == ErrorInvalidId && sceSslDisableVerifyOption(untrusted, CnCheck) == ErrorInvalidId + && sceSslSetMinSslVersion(untrusted, VersionTls12) == ErrorInvalidId && sceSslSetVerifyCallback(untrusted, nullptr, nullptr) == ErrorInvalidId, + "a deleted connection did not report an invalid id"); const int unverifiedSocket = sockets.Open(server.port); const int unverified = sceSslCreateConnection(empty, unverifiedSocket, "localhost"); @@ -322,13 +384,14 @@ int main(int argc, char** argv) { const int accepted = sceSslCreateConnection(empty, acceptedSocket, "localhost"); Require(sceSslSetVerifyCallback(accepted, RecordingCallback, &accepting) == 0, "set the accepting verify callback"); Require(sceSslConnect(accepted) == 0, "a verify callback that returns 0 did not accept the connection"); - Require(accepting.connection == accepted && accepting.error == static_cast(ErrorUnknownCa), - "the verify callback did not receive the connection and its verification result"); - Require(accepting.count == 1 && accepting.commonName == "CN=localhost", "the verify callback did not receive the peer chain"); + Require(accepting.id == empty && accepting.error == KnownCaCheck, "the verify callback did not receive the context id and the failed option"); + Require(accepting.count == 1 && accepting.commonName == "commonName=localhost", "the verify callback did not receive the peer chain"); + Require(sceSslConnect(accepted) == 0 && accepting.calls == 1, "a second connect after an accepted callback ran the callback again"); Require(sceSslRead(accepted, scratch.data(), scratch.size(), 0) == 0, "an accepted connection was not readable"); Require(sceSslDeleteConnection(accepted) == 0 && sockets.Close(acceptedSocket) == 0, "release the accepted connection"); Require(Throws([&] { sceSslGetSubjectName(empty, accepting.leaf); }), "a peer certificate handle outlived its connection"); Require(sceSslTerm_nid_postfix(empty) == 0, "release the context without CA"); + Require(sceSslTerm_nid_postfix(empty) == ErrorInvalidId, "a second context termination did not report an invalid id"); const int context = sceSslInit_nid_postfix(1024 * 1024); std::string root = RootPem; @@ -340,8 +403,9 @@ int main(int argc, char** argv) { Require(sceSslLoadCert(context, 1, &authorities, &certificate, &key) == 0, "load the fixture root and client certificate"); const auto loaded = InUse(context); + Require(sceSslCreateConnection(context, 1000, nullptr) == ErrorInvalidValue, "a null host was accepted"); const int closed = sceSslCreateConnection(context, 1000, "localhost"); - Require(Throws([&] { sceSslConnect(closed); }), "a handshake over a closed descriptor was attempted"); + Require(sceSslConnect(closed) == ErrorNetBadDescriptor, "a handshake over a closed descriptor did not report the bad descriptor"); Require(sceSslClose(closed) == 0, "close a connection that never connected"); Require(sceSslDeleteConnection(closed) == 0, "delete connection over a closed descriptor"); @@ -363,32 +427,45 @@ int main(int argc, char** argv) { const int rejected = sceSslCreateConnection(context, rejectedSocket, "localhost"); Require(sceSslSetVerifyCallback(rejected, RecordingCallback, &rejecting) == 0, "set the rejecting verify callback"); Require(sceSslConnect(rejected) == ErrorCallbackCheck, "a negative verify callback result did not fail the connect"); - Require(rejecting.error == 0 && rejecting.count == 1, "the verify callback did not see a passed chain"); - Require(sceSslConnect(rejected) == ErrorCallbackCheck, "a second connect did not repeat the callback result"); - Require(Throws([&] { sceSslRead(rejected, scratch.data(), 1, 0); }), "a rejected connection was readable"); + Require(rejecting.error == 0 && rejecting.count == 1 && rejecting.id == context, "the verify callback did not see a passed chain"); + Require(sceSslConnect(rejected) == ErrorCallbackCheck && rejecting.calls == 2, "a second connect did not run the callback again"); + Require(sceSslRead(rejected, scratch.data(), 1, 0) == ErrorCallbackCheck && rejecting.calls == 3, "a read after the rejection did not run the callback again"); + Require(sceSslWrite(rejected, scratch.data(), 1, 0) == ErrorCallbackCheck && rejecting.calls == 4, "a write after the rejection did not run the callback again"); + Require(sceSslClose(rejected) == 0, "close the rejected connection"); Require(sceSslDeleteConnection(rejected) == 0 && sockets.Close(rejectedSocket) == 0, "release the rejected connection"); + const int earlySocket = sockets.Open(server.port); + const int early = sceSslCreateConnection(context, earlySocket, "localhost"); + Require(sceSslClose(early) == 0, "close before the handshake"); + Require(sceSslRead(early, scratch.data(), scratch.size(), 0) == 0, "a read before the handshake did not run it and reach the close notify"); + Require(sceSslConnect(early) == 0, "a connect after the read-driven handshake did not return 0"); + Require(sceSslDeleteConnection(early) == 0 && sockets.Close(earlySocket) == 0, "release the early connection"); + const int descriptor = sockets.Open(server.port); const int connection = sceSslCreateConnection(context, descriptor, "localhost"); std::array buffer{}; + Require(sceSslSetMinSslVersion(connection, 0) == ErrorInvalidValue && sceSslSetMinSslVersion(connection, 6) == ErrorInvalidValue + && sceSslSetMinSslVersion(connection, 2) == 0, "the accepted minimum versions changed"); + Require(sceSslEnableVerifyOption(connection, 0x40) == ErrorInvalidValue && sceSslDisableVerifyOption(connection, 0x1000) == ErrorInvalidValue + && sceSslEnableVerifyOption(connection, 0x3f) == 0, "the verify option mask changed"); + Require(sceSslSetAlpn(connection, "") == ErrorBadLength, "an empty ALPN list was accepted"); Require(sceSslSetMinSslVersion(connection, VersionTls12) == 0 && sceSslSetAlpn(connection, "h2,http/1.1") == 0, "set version and ALPN"); Require(sceSslGetAlpnSelected(connection) == nullptr, "a protocol was selected before the handshake"); - Require(Throws([&] { sceSslRead(connection, buffer.data(), buffer.size(), 0); }), "read before the handshake"); - Require(Throws([&] { sceSslWrite(connection, buffer.data(), buffer.size(), 0); }), "write before the handshake"); Require(sceSslConnect(connection) == 0, "handshake with the loopback server"); Require(InUse(context) > loaded, "the connection is not charged to the context pool"); Require(sceSslConnect(connection) == 0, "a second connect after success did not return the same result"); const char* selected = sceSslGetAlpnSelected(connection); Require(selected && std::strcmp(selected, "http/1.1") == 0, "the negotiated ALPN protocol is not reported"); - Require(Throws([&] { sceSslWrite(connection, buffer.data(), 1, 1); }), "unknown write flags were accepted"); - Require(Throws([&] { sceSslRead(connection, buffer.data(), 1, 3); }), "peek was accepted"); + Require(sceSslWrite(connection, buffer.data(), 1, 1) == ErrorInvalidValue, "unknown write flags were accepted"); char request[] = "ping"; Require(sceSslWrite(connection, request, 4, 0) == 4, "write the request"); - Require(sceSslRead(connection, buffer.data(), buffer.size(), 0) == 4 && std::memcmp(buffer.data(), "pong", 4) == 0, "read the reply"); + Require(sceSslRead(connection, buffer.data(), 2, 1) == 2 && std::memcmp(buffer.data(), "po", 2) == 0, "a partial read did not return the available bytes"); + Require(sceSslRead(connection, buffer.data(), buffer.size(), 0) == 2 && std::memcmp(buffer.data(), "ng", 2) == 0, "a full read did not stop at the end of the stream"); Require(sceSslRead(connection, buffer.data(), buffer.size(), 1) == 0, "close notify did not end the stream"); Require(sceSslClose(connection) == 0, "close the connection"); - Require(Throws([&] { sceSslRead(connection, buffer.data(), buffer.size(), 0); }), "a closed connection was readable"); - Require(Throws([&] { sceSslConnect(connection); }), "a closed connection reconnected"); + Require(sceSslRead(connection, buffer.data(), buffer.size(), 0) == 0, "a read after the close did not return 0"); + Require(sceSslConnect(connection) == 0, "a connect after the close did not return 0"); + Require(selected && std::strcmp(sceSslGetAlpnSelected(connection), "http/1.1") == 0, "the ALPN protocol was forgotten after the close"); Require(sceSslClose(connection) == 0, "a second close failed"); Require(sceSslDeleteConnection(connection) == 0, "delete the connection"); Require(InUse(context) == loaded, "the connection's pool memory was not returned"); @@ -404,9 +481,10 @@ int main(int argc, char** argv) { Require(++rounds < WaitRounds, "a non-blocking handshake did not finish"); } Require(sceSslRead(polling, buffer.data(), buffer.size(), 0) == ErrorWantPollIn, "a non-blocking read with nothing to read did not ask to poll"); + Require(sceSslRead(polling, buffer.data(), buffer.size(), 1) == ErrorWantPollIn, "a non-blocking partial read with nothing to read did not ask to poll"); Require(sceSslWrite(polling, request, 4, 0) == 4, "write the non-blocking request"); int received; - for (rounds = 0; (received = sceSslRead(polling, buffer.data(), buffer.size(), 0)) == ErrorWantPollIn; Settle()) + for (rounds = 0; (received = sceSslRead(polling, buffer.data(), buffer.size(), 1)) == ErrorWantPollIn; Settle()) Require(++rounds < WaitRounds, "the non-blocking reply did not arrive"); Require(received == 4 && std::memcmp(buffer.data(), "pong", 4) == 0, "read the non-blocking reply"); for (rounds = 0; (received = sceSslRead(polling, buffer.data(), buffer.size(), 0)) == ErrorWantPollIn; Settle()) @@ -419,22 +497,58 @@ int main(int argc, char** argv) { const int dropped = sceSslCreateConnection(context, droppedSocket, "localhost"); Require(sceSslConnect(dropped) == 0, "handshake before the server drops the connection"); Require(sceSslRead(dropped, buffer.data(), buffer.size(), 0) == 0, "a stream that ended without close notify did not end the read"); + Require(sceSslRead(dropped, buffer.data(), buffer.size(), 1) == 0, "a second read after the drop did not return 0"); + Require(sceSslClose(dropped) == 0, "close after the drop"); Require(sceSslDeleteConnection(dropped) == 0 && sockets.Close(droppedSocket) == 0, "release the dropped connection"); const int resetSocket = sockets.Open(server.port); const int reset = sceSslCreateConnection(context, resetSocket, "localhost"); Require(sceSslConnect(reset) == 0, "handshake before the server resets the connection"); Require(sceSslWrite(reset, request, 1, 0) == 1, "write the byte the server resets after"); - Require(sceSslRead(reset, buffer.data(), buffer.size(), 0) == ErrorSocketClosed, "a reset did not report the closed socket"); + Require(sceSslRead(reset, buffer.data(), buffer.size(), 0) == ErrorNetReset, "a reset did not report the libSceNet reset code"); + Require(sceSslRead(reset, buffer.data(), buffer.size(), 1) == ErrorNetReset, "a second read after the reset changed the code"); + Require(sceSslWrite(reset, request, 4, 0) == ErrorNetPipe, "a write after the reset did not report the broken pipe"); + Require(sceSslConnect(reset) == 0, "a connect after the reset did not return 0"); + Require(sceSslClose(reset) == ErrorNetReset && sceSslClose(reset) == ErrorNetReset, "a close after the reset did not report the reset twice"); Require(sceSslDeleteConnection(reset) == 0 && sockets.Close(resetSocket) == 0, "release the reset connection"); + const int alertedSocket = sockets.Open(server.port); + const int alerted = sceSslCreateConnection(context, alertedSocket, "localhost"); + Require(sceSslConnect(alerted) == ErrorFatalAlert, "a fatal alert during the handshake did not report it"); + Require(sceSslConnect(alerted) == 0, "a connect after the fatal alert did not return 0"); + Require(sceSslRead(alerted, buffer.data(), buffer.size(), 0) == 0, "a read after the fatal alert did not return 0"); + Require(sceSslWrite(alerted, request, 4, 0) == ErrorSocketClosed, "a write after the fatal alert did not report the closed socket"); + Require(sceSslClose(alerted) == 0 && sceSslClose(alerted) == 0, "close after the fatal alert"); + Require(sceSslDeleteConnection(alerted) == 0 && sockets.Close(alertedSocket) == 0, "release the alerted connection"); + + const int expiredSocket = sockets.Open(server.port); + const int expired = sceSslCreateConnection(context, expiredSocket, "localhost"); + Require(sceSslConnect(expired) == 0, "an expired server certificate failed with the initial verify options"); + Require(sceSslRead(expired, buffer.data(), buffer.size(), 0) == 0, "close notify did not end the expired stream"); + Require(sceSslDeleteConnection(expired) == 0 && sockets.Close(expiredSocket) == 0, "release the expired connection"); + const int expiredCheckedSocket = sockets.Open(server.port); + const int expiredChecked = sceSslCreateConnection(context, expiredCheckedSocket, "localhost"); + Require(sceSslEnableVerifyOption(expiredChecked, NotAfterCheck) == 0, "enable the expiry check"); + Require(sceSslConnect(expiredChecked) == ErrorNotAfterCheck, "an expired server certificate passed the enabled expiry check"); + Require(sceSslDeleteConnection(expiredChecked) == 0 && sockets.Close(expiredCheckedSocket) == 0, "release the checked expired connection"); + const int futureSocket = sockets.Open(server.port); + const int future = sceSslCreateConnection(context, futureSocket, "localhost"); + Require(sceSslEnableVerifyOption(future, NotAfterCheck) == 0, "enable the expiry check on the future connection"); + Require(sceSslConnect(future) == 0, "a not yet valid server certificate failed without the start check"); + Require(sceSslDeleteConnection(future) == 0 && sockets.Close(futureSocket) == 0, "release the future connection"); + const int futureCheckedSocket = sockets.Open(server.port); + const int futureChecked = sceSslCreateConnection(context, futureCheckedSocket, "localhost"); + Require(sceSslEnableVerifyOption(futureChecked, NotBeforeCheck) == 0, "enable the start check"); + Require(sceSslConnect(futureChecked) == ErrorNotBeforeCheck, "a not yet valid server certificate passed the enabled start check"); + Require(sceSslDeleteConnection(futureChecked) == 0 && sockets.Close(futureCheckedSocket) == 0, "release the checked future connection"); + if (sockets.net) { const int timedSocket = sockets.Open(server.port); const GuestTimeval timeout{0, 200000}; Require(sockets.Set(timedSocket, GuestReceiveTimeout, &timeout, sizeof(timeout)) == 0, "set the receive timeout"); const int timed = sceSslCreateConnection(context, timedSocket, "localhost"); Require(sceSslConnect(timed) == 0, "handshake over the socket with a receive timeout"); - Require(sceSslRead(timed, buffer.data(), buffer.size(), 0) == ErrorReadTimeout, "an expired receive timeout did not report the timeout"); + Require(sceSslRead(timed, buffer.data(), buffer.size(), 0) == ErrorWantPollIn, "an expired receive timeout did not ask to poll"); Require(sceSslWrite(timed, request, 4, 0) == 4, "write the request after a timeout"); Require(sceSslRead(timed, buffer.data(), buffer.size(), 0) == 4 && std::memcmp(buffer.data(), "pong", 4) == 0, "read the reply after a timeout"); Require(sceSslRead(timed, buffer.data(), buffer.size(), 0) == 0, "close notify did not end the timed stream"); diff --git a/core/libs/tests/GuestStatvfs.cpp b/core/libs/tests/GuestStatvfs.cpp index ad1f8e5de0..2019801dc0 100644 --- a/core/libs/tests/GuestStatvfs.cpp +++ b/core/libs/tests/GuestStatvfs.cpp @@ -102,6 +102,8 @@ int main() { RemovePathAlias_nid_no_patch("/anyps5-statvfs-unavailable"); Require(statvfs_nid_postfix("/anyps5-statvfs-test/file.txt", nullptr) == -1); Require(*__error_nid_postfix() == 14); + Require(statvfs_nid_postfix("/anyps5-statvfs-test/missing", nullptr) == -1); + Require(*__error_nid_postfix() == 14); const auto unicodeFile = root / std::filesystem::path(u8"\u65e5.txt"); { diff --git a/core/libs/tests/GuestStdioVariadic.cpp b/core/libs/tests/GuestStdioVariadic.cpp new file mode 100644 index 0000000000..62ead8be7f --- /dev/null +++ b/core/libs/tests/GuestStdioVariadic.cpp @@ -0,0 +1,114 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +extern FileStream* __stdinp_nid_postfix; +int APS5_VABI vfscanf_nid_postfix(FileStream*, const char*, void*); +int APS5_VABI vscanf_nid_postfix(const char*, void*); +int APS5_VABI scanf_nid_postfix(const char*, ...); +int APS5_VABI vfwprintf_nid_postfix(FileStream*, const char16_t*, void*); +int APS5_VABI fwprintf_nid_postfix(FileStream*, const char16_t*, ...); +int APS5_VABI vswscanf_nid_postfix(const char16_t*, const char16_t*, void*); +int APS5_VABI vswscanf_s_nid_postfix(const char16_t*, const char16_t*, void*); +} + +#ifdef _WIN32 +#define APS5_VA_BEGIN(last) __builtin_sysv_va_list args; __builtin_sysv_va_start(args, last) +#define APS5_VA_END() __builtin_sysv_va_end(args) +#else +#define APS5_VA_BEGIN(last) std::va_list args; va_start(args, last) +#define APS5_VA_END() va_end(args) +#endif + +static void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", what); + std::abort(); +} + +static int APS5_VABI ScanStream(FileStream* stream, const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfscanf_nid_postfix(stream, format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanInput(const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vscanf_nid_postfix(format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI PrintWide(FileStream* stream, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfwprintf_nid_postfix(stream, format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanWide(const char16_t* input, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vswscanf_nid_postfix(input, format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanWideSized(const char16_t* input, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vswscanf_s_nid_postfix(input, format, args); + APS5_VA_END(); + return result; +} + +static std::string Contents(FileStream& stream) { + std::fflush(stream.GetHandle()); + std::rewind(stream.GetHandle()); + std::string text; + for (int c; (c = std::fgetc(stream.GetHandle())) != EOF;) text.push_back(static_cast(c)); + return text; +} + +int main() { + FileStream scanned(std::tmpfile()); + Require(std::fputs("42 answer 7", scanned.GetHandle()) >= 0, "seed the scanned stream"); + std::rewind(scanned.GetHandle()); + int number = 0; + char word[16]{}; + Require(ScanStream(&scanned, "%d %15s", &number, word) == 2, "vfscanf assigns both conversions"); + Require(number == 42 && std::strcmp(word, "answer") == 0, "vfscanf values"); + Require(ScanStream(&scanned, "%d", &number) == 1 && number == 7, "vfscanf continues from the stream position"); + Require(ScanStream(&scanned, "%d", &number) == EOF && number == 7, "vfscanf at end of file"); + scanned.Close(); + + FileStream input(std::tmpfile()); + Require(std::fputs("-5 12", input.GetHandle()) >= 0, "seed the standard input stream"); + std::rewind(input.GetHandle()); + FileStream* const standardInput = __stdinp_nid_postfix; + __stdinp_nid_postfix = &input; + int first = 0, second = 0; + Require(ScanInput("%d", &first) == 1 && first == -5, "vscanf reads the guest standard input"); + Require(scanf_nid_postfix("%d", &second) == 1 && second == 12, "scanf reads the guest standard input"); + __stdinp_nid_postfix = standardInput; + input.Close(); + + 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"); + written.Close(); + + 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"); + char16_t letters[4]{}; + Require(ScanWideSized(u"abcdef", u"%3ls", letters, 4u) == 1 && std::u16string(letters) == u"abc", "vswscanf_s %ls with a size"); + Require(ScanWideSized(u"abcdef", u"%ls", letters, 4u) == 0 && letters[0] == 0, "vswscanf_s rejects a too small buffer"); + return 0; +} diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index e843751cd4..b2d98ae513 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -13,12 +14,15 @@ char* APS5_VABI basename_nid_postfix(const char*); char* APS5_VABI __inet_ntoa_nid_postfix(unsigned int); char* APS5_VABI dirname_nid_postfix(const char*); int* APS5_VABI __error_nid_postfix(); +long long APS5_VABI strtonum_nid_postfix(const char*, long long, long long, const char**); int APS5_VABI strcasecmp_nid_postfix(const char*, const char*); int APS5_VABI strncasecmp_nid_postfix(const char*, const char*, std::size_t); std::size_t APS5_VABI strnlen_nid_postfix(const char*, std::size_t); std::size_t APS5_VABI strnlen_s_nid_postfix(const char*, std::size_t); char* APS5_VABI strncat_nid_postfix(char*, const char*, std::size_t); char* APS5_VABI strpbrk_nid_postfix(const char*, const char*); +const char* APS5_VABI strchr_nid_postfix(const char*, int); +char* APS5_VABI strrchr_nid_postfix(const char*, int); std::size_t APS5_VABI strcspn_nid_postfix(const char*, const char*); std::size_t APS5_VABI strlcat_nid_postfix(char*, const char*, std::size_t); char* APS5_VABI stpcpy_nid_postfix(char*, const char*); @@ -262,9 +266,68 @@ static void CheckBcopy() { bcopy_nid_postfix(nullptr, nullptr, 0); } +static void CheckStrtonum() { + struct Valid { + const char* text; + long long value; + }; + for (const auto& test : {Valid{"0", 0}, Valid{"-0", 0}, Valid{" +42", 42}, Valid{"010", 10}, + Valid{"9223372036854775807", LLONG_MAX}, Valid{"-9223372036854775808", LLONG_MIN}}) { + const char* error = "unchanged"; + *__error_nid_postfix() = 13; + Require(strtonum_nid_postfix(test.text, LLONG_MIN, LLONG_MAX, &error) == test.value); + Require(error == nullptr && *__error_nid_postfix() == 13); + } + for (const auto& test : {Valid{"-5", -5}, Valid{"5", 5}, Valid{"0", 0}}) { + const char* error = "unchanged"; + Require(strtonum_nid_postfix(test.text, -5, 5, &error) == test.value && error == nullptr); + Require(strtonum_nid_postfix(test.text, test.value, test.value, &error) == test.value && error == nullptr); + } + const auto fails = [](const char* text, long long minimum, long long maximum, const char* expected, int number) { + const char* error = nullptr; + *__error_nid_postfix() = 13; + Require(strtonum_nid_postfix(text, minimum, maximum, &error) == 0); + Require(error != nullptr && std::strcmp(error, expected) == 0 && *__error_nid_postfix() == number); + *__error_nid_postfix() = 13; + Require(strtonum_nid_postfix(text, minimum, maximum, nullptr) == 0 && *__error_nid_postfix() == number); + }; + for (const char* text : {"", " ", "+", "-", "--1", "0x10", "0b10", "1x", "42 ", "1\n", + "9223372036854775808x", "-9223372036854775809x"}) + fails(text, LLONG_MIN, LLONG_MAX, "invalid", 22); + fails("0", 1, -1, "invalid", 22); + fails(nullptr, 1, -1, "invalid", 22); + fails("9223372036854775808", 1, -1, "invalid", 22); + fails("-6", -5, 5, "too small", 34); + fails("6", -5, 5, "too large", 34); + fails("0", 1, 5, "too small", 34); + fails("0", -5, -1, "too large", 34); + fails("-9223372036854775809", LLONG_MIN, LLONG_MAX, "too small", 34); + fails("9223372036854775808", LLONG_MIN, LLONG_MAX, "too large", 34); + fails("-9999999999999999999999999999999999", LLONG_MIN, LLONG_MAX, "too small", 34); + fails("9999999999999999999999999999999999", LLONG_MIN, LLONG_MAX, "too large", 34); + *__error_nid_postfix() = 34; + Require(strtonum_nid_postfix("42", 0, 100, nullptr) == 42 && *__error_nid_postfix() == 34); +} + +static void CheckCharacterConversionInSearches() { + const char bytes[] = "a\xe9" "b\xe9" "c"; + Require(strchr_nid_postfix(bytes, 0xe9) == bytes + 1); + Require(strchr_nid_postfix(bytes, 0x1e9) == bytes + 1); + Require(strchr_nid_postfix(bytes, -23) == bytes + 1); + Require(strrchr_nid_postfix(bytes, 0xe9) == bytes + 3); + Require(strrchr_nid_postfix(bytes, 0x1e9) == bytes + 3); + Require(strrchr_nid_postfix(bytes, -23) == bytes + 3); + const char path[] = "a/b/c"; + Require(strrchr_nid_postfix(path, 0x12f) == path + 3); + Require(strrchr_nid_postfix(path, 0) == path + 5); + Require(strrchr_nid_postfix(path, 'z') == nullptr); +} + int main() { + CheckStrtonum(); CheckBcopy(); if (!CheckDirnameLengths()) return 1; + CheckCharacterConversionInSearches(); CheckBoundsCheckedFunctions(); CheckCaseInsensitiveComparison(); CheckSscanfS(); diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index ddc6fcbdb9..f98548f2ce 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -26,6 +26,7 @@ int APS5_VABI sceSysmoduleLoadModuleInternal(std::uint32_t id); int APS5_VABI sceSysmoduleUnloadModuleInternal(std::uint32_t id); int APS5_VABI sceSysmoduleIsLoadedInternal(std::uint32_t id); int APS5_VABI sceSysmoduleLoadModuleInternalWithArg(std::uint32_t id, int argc, void* argv, std::uint64_t unk, int* ret); +int APS5_VABI sceSysmoduleUnloadModuleInternalWithArg(std::uint32_t id, int argc, void* argv, std::uint64_t unk, int* ret); int APS5_VABI sceSysmoduleGetModuleInfoForUnwind(std::uint64_t addr, int flags, ModuleInfoForUnwind* info); int APS5_VABI sceSysmoduleGetModuleHandleInternal(std::uint32_t id, std::int32_t* handle); int APS5_VABI sceKernelDlsym(std::int32_t handle, const char* symbol, void** addr); @@ -138,6 +139,9 @@ void CheckUnknownIds() { int result = 0x5a5a5a5a; Require(ThrowsRuntimeError([internalId, &result] { sceSysmoduleLoadModuleInternalWithArg(internalId, 0, nullptr, 0, &result); })); Require(result == 0x5a5a5a5a); + int unloadResult = 0x5a5a5a5a; + Require(ThrowsRuntimeError([internalId, &unloadResult] { sceSysmoduleUnloadModuleInternalWithArg(internalId, 0, nullptr, 0, &unloadResult); })); + Require(unloadResult == 0x5a5a5a5a); } #ifdef _WIN32 @@ -313,6 +317,19 @@ int main() { Require(sceSysmoduleUnloadModuleInternal(kHdrScopesInternalModuleId) == 0); Require(sceSysmoduleUnloadModuleInternal(kHdrScopesInternalModuleId) == kModuleUnloaded); + int unloadArgResult = -1; + Require(sceSysmoduleUnloadModuleInternalWithArg(0, 0, nullptr, 0, &unloadArgResult) == kInvalidValue); + Require(unloadArgResult == -1); + Require(sceSysmoduleUnloadModuleInternalWithArg(kAgcDriverModuleId, 0, nullptr, 0, &unloadArgResult) == kModuleUnloaded); + Require(unloadArgResult == -1); + int loadArgResult = -1; + Require(sceSysmoduleLoadModuleInternalWithArg(kAgcDriverModuleId, 0, nullptr, 0, &loadArgResult) == 0); + Require(loadArgResult == 0); + Require(sceSysmoduleIsLoadedInternal(kAgcDriverModuleId) == 0); + Require(sceSysmoduleUnloadModuleInternalWithArg(kAgcDriverModuleId, 0, nullptr, 0, &unloadArgResult) == 0); + Require(unloadArgResult == 0); + Require(sceSysmoduleIsLoadedInternal(kAgcDriverModuleId) == kModuleUnloaded); + Require(sceSysmoduleIsLoadedInternal(kAgcModuleId) == kModuleUnloaded); Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == kModuleUnloaded); Require(sceSysmoduleLoadModuleInternal(kAgcModuleId) == 0); diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 9a9f496c0e..5c08d7e2d4 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -258,7 +258,7 @@ static void CheckSystemStringNames() { } } static void CheckLeafWithTrailingComponent() { - const int leaves[][2] = {{6, 3}, {6, 2}, {6, 5}, {6, 7}, {1, 37}}; + const int leaves[][2] = {{6, 3}, {6, 2}, {6, 5}, {6, 7}, {1, 37}, {1, 50}}; for (const auto& leaf : leaves) { const int mib[] = {leaf[0], leaf[1], 0}; char buffer[8] = {}; @@ -269,6 +269,49 @@ static void CheckLeafWithTrailingComponent() { Require(length == sizeof(buffer)); } } +static void CheckMeasuredAbsentSysctlMibs() { + static constexpr struct { + int mib[3]; + std::uint32_t length; + } cases[] = { + {{1, 2147482816, 2147482814}, 3}, + {{1, 2147483151}, 2}, + {{1, 2147483182, 2147483181}, 3}, + {{1, 2147482838, 2147482834}, 3}, + }; + for (const auto& entry : cases) { + unsigned char buffer[64]; + std::memset(buffer, 0xaa, sizeof(buffer)); + std::size_t length = sizeof(buffer); + *__error_nid_postfix() = 77; + Require(sysctl_nid_postfix(entry.mib, entry.length, buffer, &length, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2 && length == sizeof(buffer)); + for (const auto byte : buffer) Require(byte == 0xaa); + + length = 77; + *__error_nid_postfix() = 77; + Require(sysctl_nid_postfix(entry.mib, entry.length, nullptr, &length, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2 && length == 77); + + std::memset(buffer, 0xaa, sizeof(buffer)); + *__error_nid_postfix() = 77; + Require(sysctl_nid_postfix(entry.mib, entry.length, buffer, nullptr, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2); + for (const auto byte : buffer) Require(byte == 0xaa); + + *__error_nid_postfix() = 77; + Require(sysctl_nid_postfix(entry.mib, entry.length, nullptr, nullptr, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2); + + bool threw = false; + try { + sysctl_nid_postfix(entry.mib, entry.length, buffer, &length, buffer, sizeof(buffer)); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + } +} static void CheckSystemSwVersion() { *__error_nid_postfix() = 13; Require(sceKernelGetSystemSwVersion_nid_postfix(nullptr) == SCE_KERNEL_ERROR_EFAULT && *__error_nid_postfix() == 13); @@ -293,23 +336,50 @@ static void CheckSystemSwVersion() { } } static void CheckPseudoRandomSysctl() { + unsigned char first[128]; + unsigned char second[128]; + const auto untouchedFrom = [&first](std::size_t from) { + for (std::size_t i = from; i < sizeof(first); ++i) + if (first[i] != 0xaa) return false; + return true; + }; *__error_nid_postfix() = 13; - unsigned char first[64] = {}; - unsigned char second[64] = {}; - std::size_t length = sizeof(first); - Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, nullptr, 0) == 0 && length == sizeof(first)); - length = sizeof(second); - Require(sysctlbyname_nid_postfix("kern.rng_pseudo", second, &length, nullptr, 0) == 0 && length == sizeof(second)); - Require(std::memcmp(first, second, sizeof(first)) != 0); + std::memset(first, 0xaa, sizeof(first)); + std::memset(second, 0xaa, sizeof(second)); + std::size_t length = 64; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, nullptr, 0) == 0 && length == 64 && untouchedFrom(64) && !untouchedFrom(0)); + length = 64; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", second, &length, nullptr, 0) == 0 && length == 64); + Require(std::memcmp(first, second, 64) != 0); Require(*__error_nid_postfix() == 13); - Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, first, sizeof(first)) == -1 && *__error_nid_postfix() == 1); - bool threw = false; - try { - sysctlbyname_nid_postfix("kern.rng_pseudo", nullptr, &length, nullptr, 0); - } catch (const std::runtime_error&) { - threw = true; + static constexpr std::size_t larger[] = {65, 128}; + for (std::size_t asked : larger) { + std::memset(first, 0xaa, sizeof(first)); + length = asked; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, nullptr, 0) == 0 && length == 64 && untouchedFrom(64) && !untouchedFrom(0)); } - Require(threw); + Require(*__error_nid_postfix() == 13); + static constexpr std::size_t smaller[] = {0, 1, 63}; + for (std::size_t asked : smaller) { + std::memset(first, 0xaa, sizeof(first)); + length = asked; + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == asked && untouchedFrom(asked)); + if (asked == 63) Require(!untouchedFrom(0)); + } + static constexpr std::size_t presets[] = {0, 1, 64, 100, 4096}; + for (std::size_t preset : presets) { + length = preset; + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", nullptr, &length, nullptr, 0) == 0 && length == 64 && *__error_nid_postfix() == 13); + } + std::memset(first, 0xaa, sizeof(first)); + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 12 && untouchedFrom(0)); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", nullptr, nullptr, nullptr, 0) == 0 && *__error_nid_postfix() == 13); + length = 64; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, first, 64) == -1 && *__error_nid_postfix() == 1); } static void CheckOvercommitSysctl() { *__error_nid_postfix() = 13; @@ -440,6 +510,45 @@ static void CheckVmmapSysctl() { const int tooLong[] = {1, 14, 32, self, 0}; Require(sysctl_nid_postfix(tooLong, 5, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); } +static void CheckPseudoRandomMibSysctl() { + const int mib[] = {1, 50}; + *__error_nid_postfix() = 13; + std::size_t length = 7; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == 64); + Require(*__error_nid_postfix() == 13); + Require(sysctl_nid_postfix(mib, 2, nullptr, nullptr, nullptr, 0) == 0); + unsigned char bytes[80]; + std::memset(bytes, 0xaa, sizeof(bytes)); + length = 64; + Require(sysctl_nid_postfix(mib, 2, bytes, &length, nullptr, 0) == 0 && length == 64); + bool changed = false; + for (std::size_t i = 0; i < 64; ++i) changed = changed || bytes[i] != 0xaa; + Require(changed && bytes[64] == 0xaa && bytes[79] == 0xaa); + unsigned char other[64] = {}; + length = sizeof(other); + Require(sysctl_nid_postfix(mib, 2, other, &length, nullptr, 0) == 0 && length == sizeof(other)); + Require(std::memcmp(bytes, other, sizeof(other)) != 0); + + unsigned char shortBuffer[64]; + std::memset(shortBuffer, 0xaa, sizeof(shortBuffer)); + length = 63; + Require(sysctl_nid_postfix(mib, 2, shortBuffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + bool shortChanged = false; + for (std::size_t i = 0; i < 63; ++i) shortChanged = shortChanged || shortBuffer[i] != 0xaa; + Require(length == 63 && shortChanged && shortBuffer[63] == 0xaa); + std::memset(shortBuffer, 0xaa, sizeof(shortBuffer)); + Require(sysctl_nid_postfix(mib, 2, shortBuffer, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + for (unsigned char byte : shortBuffer) Require(byte == 0xaa); + int value = 0; + length = sizeof(value); + bool threw = false; + try { + sysctl_nid_postfix(mib, 2, &value, &length, &value, sizeof(value)); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); +} static void CheckNameToMib() { struct Expected { const char* name; @@ -453,6 +562,7 @@ static void CheckNameToMib() { {"hw.physmem", {6, 5}, 2}, {"hw.realmem", {6, 12}, 2}, {"kern.arandom", {1, 37}, 2}, + {"kern.rng_pseudo", {1, 50}, 2}, {"kern.proc.pathname", {1, 14, 12}, 3}, }; *__error_nid_postfix() = 13; @@ -496,6 +606,17 @@ static void CheckNameToMib() { Require(sysctlnametomib_nid_postfix("hw.ncpu", truncated, &size) == -1 && *__error_nid_postfix() == 12); Require(size == 0 && truncated[0] == 6); + int rngMib[3] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + size = 3; + Require(sysctlnametomib_nid_postfix("kern.rng_pseudo", rngMib, &size) == 0 && size == 2); + Require(rngMib[0] == 1 && rngMib[1] == 50 && rngMib[2] == 0x5a5a5a5a); + size = 2; + Require(sysctl_nid_postfix(rngMib, static_cast(size), nullptr, &length, nullptr, 0) == 0 && length == 64); + rngMib[0] = rngMib[1] = rngMib[2] = 0x5a5a5a5a; + size = 1; + Require(sysctlnametomib_nid_postfix("kern.rng_pseudo", rngMib, &size) == -1 && *__error_nid_postfix() == 12); + Require(size == 1 && rngMib[0] == 1 && rngMib[1] == 0x5a5a5a5a); + size = 4; Require(sysctlnametomib_nid_postfix("", mib, &size) == -1 && *__error_nid_postfix() == 2); Require(sysctlnametomib_nid_postfix("machdep.cacheline_size", mib, &size) == -1 && *__error_nid_postfix() == 2); @@ -547,12 +668,14 @@ int main() { CheckSystemStringSysctl(); CheckSystemStringNames(); CheckLeafWithTrailingComponent(); + CheckMeasuredAbsentSysctlMibs(); CheckSystemSwVersion(); CheckPseudoRandomSysctl(); CheckSdkVersionSysctl(); CheckOvercommitSysctl(); CheckArandomSysctl(); CheckVmmapSysctl(); + CheckPseudoRandomMibSysctl(); CheckNameToMib(); *__error_nid_postfix() = 13; Require(sysconf_nid_postfix(1) == 262144); diff --git a/core/libs/tests/GuestSystemGesture.cpp b/core/libs/tests/GuestSystemGesture.cpp index a1bd7ec72c..01d5123939 100644 --- a/core/libs/tests/GuestSystemGesture.cpp +++ b/core/libs/tests/GuestSystemGesture.cpp @@ -16,6 +16,7 @@ int APS5_VABI sceSystemGestureGetPrimitiveTouchEventsCount(std::int32_t); int APS5_VABI sceSystemGestureGetTouchEvents(std::int32_t, const void*, void*, std::uint32_t, std::uint32_t*); int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByIndex(std::int32_t, std::uint32_t, void*); int APS5_VABI sceSystemGestureGetTouchEventByIndex(std::int32_t, const void*, std::uint32_t, void*); +int APS5_VABI sceSystemGestureGetTouchEventByEventID(std::int32_t, const void*, std::uint32_t, void*); int APS5_VABI sceSystemGestureAppendTouchRecognizer(std::int32_t, void*); int APS5_VABI sceSystemGestureCreateTouchRecognizer(std::int32_t, void*, std::int32_t, const void*, const void*); int APS5_VABI sceSystemGestureGetTouchRecognizerInformation(std::int32_t, const void*, void*); @@ -240,6 +241,9 @@ int main() { Require(sceSystemGestureGetTouchEventByIndex(handle, &recognizer, 0, buffer.data()) == indexOutOfArray); Require(sceSystemGestureGetTouchEventByIndex(handle + 1, &recognizer, 0, buffer.data()) == invalidHandle); Require(sceSystemGestureGetTouchEventByIndex(handle, nullptr, 0, buffer.data()) == invalidArgument); + Require(sceSystemGestureGetTouchEventByEventID(handle, &recognizer, 0, buffer.data()) == indexOutOfArray); + Require(sceSystemGestureGetTouchEventByEventID(handle + 1, &recognizer, 0, buffer.data()) == invalidHandle); + Require(sceSystemGestureGetTouchEventByEventID(handle, nullptr, 0, buffer.data()) == invalidArgument); Require(buffer == original); Require(sceSystemGestureAppendTouchRecognizer(handle, &recognizer) == 0); diff --git a/core/libs/tests/GuestTruncateExtend.cpp b/core/libs/tests/GuestTruncateExtend.cpp new file mode 100644 index 0000000000..8f04b83125 --- /dev/null +++ b/core/libs/tests/GuestTruncateExtend.cpp @@ -0,0 +1,90 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#endif + +extern "C" { +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::uint64_t); +std::int64_t APS5_VABI pread_nid_postfix(int, void*, std::uint64_t, std::int64_t); +std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); +int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); +int APS5_VABI truncate_nid_postfix(const char*, std::int64_t); +int APS5_VABI unlink_nid_postfix(const char*); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int ReadOnly = 0; +constexpr int ReadWrite = 2; +constexpr int Create = 0x200; +constexpr std::int64_t Length = 64ll * 1024 * 1024; +constexpr char FileName[] = "anyps5_truncate_extend_file"; + +void Cleanup() { + unlink_nid_postfix(FileName); +} + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + Cleanup(); + std::abort(); +} + +std::uint64_t BytesWritten() { +#ifdef _WIN32 + IO_COUNTERS counters{}; + return GetProcessIoCounters(GetCurrentProcess(), &counters) ? counters.WriteTransferCount : 0; +#else + return 0; +#endif +} + +} + +int main() { + Cleanup(); + const int descriptor = open_nid_postfix(FileName, Create | ReadWrite, 0644); + Require(descriptor >= 0, "open failed"); + Require(write_nid_postfix(descriptor, "head", 4) == 4, "write failed"); + + const std::uint64_t before = BytesWritten(); + Require(ftruncate_nid_postfix(descriptor, Length) == 0, "ftruncate to 64 MiB failed"); + const std::uint64_t written = BytesWritten() - before; + std::printf("extending by 64 MiB wrote %llu bytes\n", static_cast(written)); +#ifdef _WIN32 + Require(written < static_cast(Length) / 16, "extending a file must not write its new length as zero bytes: the process wrote about as many bytes as the new length"); +#endif + + Require(lseek_nid_postfix(descriptor, 0, 2) == Length, "the extended file has the wrong size"); + Require(lseek_nid_postfix(descriptor, 0, 1) == Length, "ftruncate must not move the file offset"); + char head[4] = {}; + char tail[8] = {1, 1, 1, 1, 1, 1, 1, 1}; + Require(pread_nid_postfix(descriptor, head, 4, 0) == 4 && std::memcmp(head, "head", 4) == 0, "extending must keep the existing bytes"); + Require(pread_nid_postfix(descriptor, tail, 8, Length - 8) == 8 && tail[0] == 0 && tail[7] == 0, "the extension must read as zero bytes"); + + Require(ftruncate_nid_postfix(descriptor, 2) == 0, "shrinking failed"); + Require(lseek_nid_postfix(descriptor, 0, 2) == 2, "the shrunk file has the wrong size"); + Require(ftruncate_nid_postfix(descriptor, -1) == -1 && *__error_nid_postfix() == 22, "a negative length must fail with EINVAL (22)"); + Require(close_nid_postfix(descriptor) == 0, "close failed"); + + const int readOnly = open_nid_postfix(FileName, ReadOnly, 0); + Require(readOnly >= 0, "reopen failed"); + Require(ftruncate_nid_postfix(readOnly, 8) == -1 && *__error_nid_postfix() == 22, "ftruncate on a read-only descriptor must fail with EINVAL (22)"); + Require(close_nid_postfix(readOnly) == 0, "close failed"); + + Require(truncate_nid_postfix(FileName, Length) == 0, "truncate to 64 MiB failed"); + const int check = open_nid_postfix(FileName, ReadOnly, 0); + Require(check >= 0 && lseek_nid_postfix(check, 0, 2) == Length, "truncate by path left the wrong size"); + Require(close_nid_postfix(check) == 0, "close failed"); + Cleanup(); + return 0; +} diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index e9ffebd9a7..f9e50c0e20 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -110,12 +110,6 @@ static void CheckDontWait() { char buffer[16]{}; Iovec vector{buffer, sizeof(buffer)}; Msghdr message{nullptr, 0, &vector, 1, nullptr, 0, 0}; -#ifdef _WIN32 - Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 45); - Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 0x80, nullptr, nullptr) == -1 && *__error_nid_postfix() == 45); - Require(recvmsg_nid_postfix(receiver, &message, 0x80) == -1 && *__error_nid_postfix() == 45); - Require(sendto_nid_postfix(sender, "x", 1, 0x80, address.data(), address.size()) == -1 && *__error_nid_postfix() == 45); -#else const auto start = std::chrono::steady_clock::now(); Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 35); Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 0x80, nullptr, nullptr) == -1 && *__error_nid_postfix() == 35); @@ -134,7 +128,6 @@ static void CheckDontWait() { std::memset(buffer, 0, sizeof(buffer)); Require(recvmsg_nid_postfix(receiver, &message, 0) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 35); -#endif Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x1) == -1 && *__error_nid_postfix() == 45); Require(close_nid_postfix(receiver) == 0 && close_nid_postfix(sender) == 0); } diff --git a/core/libs/tests/GuestUltCondition.cpp b/core/libs/tests/GuestUltCondition.cpp new file mode 100644 index 0000000000..51741e4aa2 --- /dev/null +++ b/core/libs/tests/GuestUltCondition.cpp @@ -0,0 +1,238 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceUltFinalize(); +int APS5_VABI sceUltMutexCreate(void*, const char*, void*, const UltMutexOptParam*, std::uint32_t); +int APS5_VABI sceUltMutexLock(void*); +int APS5_VABI sceUltMutexUnlock(void*); +int APS5_VABI sceUltMutexDestroy(void*); +int APS5_VABI _sceUltConditionVariableCreate(void*, const char*, void*, const void*, std::uint32_t); +int APS5_VABI sceUltConditionVariableWait(void*); +int APS5_VABI sceUltConditionVariableSignal(void*); +int APS5_VABI sceUltConditionVariableDestroy(void*); +} + +constexpr int Null = -2139029503; +constexpr int Invalid = -2139029500; +constexpr int State = -2139029498; +constexpr int Busy = -2139029497; + +static void Require(bool ok, int line) { + if (!ok) { + std::fprintf(stderr, "ULT condition check failed at line %d\n", line); + std::abort(); + } +} + +#define CHECK(value) Require((value), __LINE__) + +struct alignas(8) Object { std::array bytes{}; }; + +struct Fixture { + Object mutex; + Object condition; + + Fixture() { + CHECK(sceUltMutexCreate(&mutex, "mutex", nullptr, nullptr, 0x12000000) == 0); + CHECK(_sceUltConditionVariableCreate(&condition, "condition", &mutex, nullptr, 0x12000000) == 0); + } + + void Lock() { CHECK(sceUltMutexLock(&mutex) == 0); } + void Unlock() { CHECK(sceUltMutexUnlock(&mutex) == 0); } + void Wait() { CHECK(sceUltConditionVariableWait(&condition) == 0); } + void Signal() { CHECK(sceUltConditionVariableSignal(&condition) == 0); } + + ~Fixture() { + CHECK(sceUltConditionVariableDestroy(&condition) == 0); + CHECK(sceUltMutexDestroy(&mutex) == 0); + } +}; + +static void Validation() { + Fixture f; + Object unused; + CHECK(sceUltMutexDestroy(nullptr) == Null); + CHECK(sceUltMutexDestroy(&unused) == State); + CHECK(_sceUltConditionVariableCreate(nullptr, nullptr, nullptr, nullptr, 0) == Null); + CHECK(_sceUltConditionVariableCreate(&unused, nullptr, &unused, nullptr, 0) == Invalid); + CHECK(_sceUltConditionVariableCreate(&f.condition, nullptr, &f.mutex, nullptr, 0) == State); + CHECK(sceUltConditionVariableDestroy(nullptr) == Null); + CHECK(sceUltConditionVariableDestroy(&unused) == State); + CHECK(sceUltConditionVariableSignal(nullptr) == Null); + CHECK(sceUltConditionVariableSignal(&unused) == State); + CHECK(sceUltConditionVariableWait(nullptr) == Null); + CHECK(sceUltConditionVariableWait(&unused) == State); + CHECK(_sceUltConditionVariableCreate(&unused, nullptr, nullptr, nullptr, 0) == Null); + CHECK(sceUltConditionVariableWait(&f.condition) == State); + CHECK(sceUltMutexUnlock(&f.mutex) == State); + bool unsupported = false; + try { _sceUltConditionVariableCreate(&unused, nullptr, &f.mutex, &unused, 0); } + catch (const std::runtime_error&) { unsupported = true; } + CHECK(unsupported); + f.Lock(); + CHECK(sceUltMutexDestroy(&f.mutex) == Busy); + CHECK(sceUltMutexCreate(&f.mutex, nullptr, nullptr, nullptr, 0) == State); + auto other = std::async(std::launch::async, [&] { + CHECK(sceUltMutexUnlock(&f.mutex) == State); + CHECK(sceUltConditionVariableWait(&f.condition) == State); + }); + other.get(); + f.Lock(); + unsupported = false; + try { f.Wait(); } catch (const std::runtime_error&) { unsupported = true; } + CHECK(unsupported); + f.Unlock(); + CHECK(sceUltMutexDestroy(&f.mutex) == Busy); + f.Unlock(); + CHECK(sceUltMutexDestroy(&f.mutex) == Busy); + CHECK(sceUltConditionVariableDestroy(&f.condition) == 0); + CHECK(sceUltMutexDestroy(&f.mutex) == 0); + CHECK(sceUltMutexLock(&f.mutex) == State); + CHECK(sceUltMutexCreate(&f.mutex, nullptr, nullptr, nullptr, 0) == 0); + CHECK(sceUltConditionVariableDestroy(&f.condition) == State); + CHECK(sceUltConditionVariableSignal(&f.condition) == State); + CHECK(_sceUltConditionVariableCreate(&f.condition, nullptr, &f.mutex, nullptr, 0) == 0); +} + +static void Await(const std::atomic& count, int expected) { + const auto end = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (count.load() != expected) { + CHECK(std::chrono::steady_clock::now() < end); + std::this_thread::yield(); + } +} + +static void Handoff() { + Fixture f; + f.Signal(); + std::atomic entered = 0; + int payload = 0; + auto waiter = std::async(std::launch::async, [&] { + f.Lock(); + entered = 1; + f.Wait(); + CHECK(payload == 42); + CHECK(sceUltMutexDestroy(&f.mutex) == Busy); + f.Unlock(); + }); + Await(entered, 1); + f.Lock(); + CHECK(sceUltConditionVariableDestroy(&f.condition) == Busy); + CHECK(sceUltMutexDestroy(&f.mutex) == Busy); + CHECK(waiter.wait_for(std::chrono::milliseconds(30)) == std::future_status::timeout); + f.Unlock(); + f.Lock(); + payload = 42; + f.Signal(); + CHECK(waiter.wait_for(std::chrono::milliseconds(30)) == std::future_status::timeout); + CHECK(sceUltConditionVariableDestroy(&f.condition) == Busy); + f.Unlock(); + CHECK(waiter.wait_for(std::chrono::seconds(3)) == std::future_status::ready); + waiter.get(); +} + +static void SignalOne() { + Fixture f; + std::atomic entered = 0, finished = 0; + std::array, 3> waiters; + for (auto& waiter : waiters) waiter = std::async(std::launch::async, [&] { + f.Lock(); + ++entered; + f.Wait(); + ++finished; + f.Unlock(); + }); + Await(entered, 3); + f.Lock(); + f.Signal(); + f.Unlock(); + Await(finished, 1); + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + CHECK(finished == 1); + f.Lock(); + f.Signal(); + f.Signal(); + f.Unlock(); + for (auto& waiter : waiters) { + CHECK(waiter.wait_for(std::chrono::seconds(3)) == std::future_status::ready); + waiter.get(); + } + CHECK(finished == 3); +} + +static void Stress() { + Fixture f; + Object consumed; + CHECK(_sceUltConditionVariableCreate(&consumed, nullptr, &f.mutex, nullptr, 0) == 0); + int payload = 0; + auto consumer = std::async(std::launch::async, [&] { + for (int expected = 1; expected <= 5000; ++expected) { + f.Lock(); + while (payload == 0) f.Wait(); + CHECK(payload == expected); + payload = 0; + CHECK(sceUltConditionVariableSignal(&consumed) == 0); + f.Unlock(); + } + }); + for (int value = 1; value <= 5000; ++value) { + f.Lock(); + while (payload != 0) CHECK(sceUltConditionVariableWait(&consumed) == 0); + payload = value; + f.Signal(); + f.Unlock(); + } + CHECK(consumer.wait_for(std::chrono::seconds(3)) == std::future_status::ready); + consumer.get(); + CHECK(sceUltConditionVariableDestroy(&consumed) == 0); +} + +static void Finalize(bool signaled) { + Object mutex, condition; + CHECK(sceUltMutexCreate(&mutex, "mutex", nullptr, nullptr, 0) == 0); + CHECK(_sceUltConditionVariableCreate(&condition, "condition", &mutex, nullptr, 0) == 0); + std::atomic entered = 0; + auto waiter = std::async(std::launch::async, [&] { + CHECK(sceUltMutexLock(&mutex) == 0); + entered = 1; + CHECK(sceUltConditionVariableWait(&condition) == State); + CHECK(sceUltMutexUnlock(&mutex) == State); + }); + Await(entered, 1); + CHECK(sceUltMutexLock(&mutex) == 0); + if (signaled) CHECK(sceUltConditionVariableSignal(&condition) == 0); + CHECK(waiter.wait_for(std::chrono::milliseconds(30)) == std::future_status::timeout); + CHECK(sceUltConditionVariableDestroy(&condition) == Busy); + CHECK(sceUltFinalize() == 0); + CHECK(waiter.wait_for(std::chrono::seconds(3)) == std::future_status::ready); + waiter.get(); + CHECK(sceUltConditionVariableWait(&condition) == State); + CHECK(sceUltMutexLock(&mutex) == State); + CHECK(sceUltMutexCreate(&mutex, "reopened", nullptr, nullptr, 0) == 0); + CHECK(_sceUltConditionVariableCreate(&condition, "reopened", &mutex, nullptr, 0) == 0); + CHECK(sceUltConditionVariableDestroy(&condition) == 0); + CHECK(sceUltMutexDestroy(&mutex) == 0); +} + +int main(int argc, char** argv) { + if (argc != 2) return 1; + const std::string test = argv[1]; + if (test == "validation") Validation(); + else if (test == "handoff") Handoff(); + else if (test == "signal-one") SignalOne(); + else if (test == "stress") Stress(); + else if (test == "finalize-queued") Finalize(false); + else if (test == "finalize-reacquire") Finalize(true); + else return 1; + CHECK(sceUltFinalize() == 0); + std::puts("ULT condition tests passed"); +} diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index b076be3735..c89c38791d 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -6,6 +6,7 @@ #include #include #include +#include extern "C" { int APS5_VABI sceUltInitialize(); @@ -36,6 +37,9 @@ int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void*); int APS5_VABI sceUltSemaphoreDestroy(void*); int APS5_VABI sceUltQueueDestroy(void*); int APS5_VABI sceUltQueueDataResourcePoolDestroy(void*); +int APS5_VABI sceUltConditionVariableDestroy(void*); +int APS5_VABI sceUltReaderWriterLockDestroy(); +int APS5_VABI sceUltGetConditionVariableInfo(); } static constexpr int Ok = 0; @@ -106,6 +110,21 @@ static void Validation() { CHECK(sceUltUlthreadJoin(&thread, &status) == Ok && status == 0x27); CHECK(sceUltFinalize() == Ok); CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(1, 1) == 256u + 16u * 1024u); + CHECK(sceUltConditionVariableDestroy(nullptr) == -2139029503); + bool threwRwl = false; + try { + sceUltReaderWriterLockDestroy(); + } catch (const std::runtime_error&) { + threwRwl = true; + } + CHECK(threwRwl); + bool threwInfo = false; + try { + sceUltGetConditionVariableInfo(); + } catch (const std::runtime_error&) { + threwInfo = true; + } + CHECK(threwInfo); } static void TryJoin() { diff --git a/core/libs/tests/GuestVideoOutOutputMode.cpp b/core/libs/tests/GuestVideoOutOutputMode.cpp index 4bed080a11..82ce4bc2fa 100644 --- a/core/libs/tests/GuestVideoOutOutputMode.cpp +++ b/core/libs/tests/GuestVideoOutOutputMode.cpp @@ -18,6 +18,7 @@ int APS5_VABI sceVideoOutConfigureOutput(int handle, std::uint64_t mode, const V static constexpr int SYSTEM_USER = 255; static constexpr int MAIN_BUS = 0; +static constexpr int HMD2_MAIN_BUS = 0x20; static constexpr int INVALID_VALUE = static_cast(0x80290001); static constexpr int INVALID_ADDRESS = static_cast(0x80290002); static constexpr int INVALID_HANDLE = static_cast(0x8029000B); @@ -38,6 +39,11 @@ int main() { for (std::size_t index = 0; index < raw.size(); ++index) Require(raw[index] == (index == 2 ? 0xFF : 0)); Require(sceVideoOutInitializeOutputOptions(nullptr) == INVALID_ADDRESS); + std::array zeroedOpenParam{}; + Require(sceVideoOutOpen(SYSTEM_USER, HMD2_MAIN_BUS, 0, nullptr) == INVALID_VALUE); + Require(sceVideoOutOpen(SYSTEM_USER, HMD2_MAIN_BUS, 0, zeroedOpenParam.data()) == INVALID_VALUE); + Require(sceVideoOutOpen(0, HMD2_MAIN_BUS, 0, nullptr) == INVALID_VALUE); + int handle = 0; try { for (int unopened : {-1, 0, 1, 0x7FFFFFFF}) { @@ -64,6 +70,10 @@ int main() { for (std::uint64_t mode : {0x0ull, 0x2ull, 0x3ull, 0x5ull, 0x6ull, 0x9ull, 0xAull, 0xBull, 0x14ull, 0x20ull, 0x3Full, 0x100ull, 0xD000000Aull, 0x8000000000000001ull, ~0ull}) { Require(sceVideoOutIsOutputSupported(handle, mode, &options, nullptr, 0) == UNKNOWN_OUTPUT_MODE); } + for (std::uint64_t mode : {0x20001ull, 0x2000Cull, 0x2000Dull, 0x2000Eull, 0x2000Full, 0x20010ull}) { + Require(sceVideoOutIsOutputSupported(handle, mode, nullptr, nullptr, 0) == UNKNOWN_OUTPUT_MODE); + Require(sceVideoOutConfigureOutput(handle, mode, nullptr, nullptr, 0) == UNKNOWN_OUTPUT_MODE); + } bool thrown = false; try { sceVideoOutIsOutputSupported(handle, 0x12, &options, nullptr, 0); diff --git a/core/libs/tests/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index 4212ef5138..91ee70f9c7 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -596,6 +596,103 @@ void testDecode(bool lengthPrefixed, bool separateParameters = false) { checkThrows([&] { sceVideodec2DeleteDecoder_nid_postfix(handle); }); } +void decodeInOrder(const std::vector>& units) { + const DecoderConfigInfo config{sizeof(DecoderConfigInfo), 0, 1, 100, 0, static_cast(Width), static_cast(Height), 4, 1}; + DecoderMemoryInfo memory{sizeof(DecoderMemoryInfo)}; + check(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&config, &memory) == 0, "memory query failed"); + std::uint64_t handle = 0; + check(sceVideodec2CreateDecoder_nid_postfix(&config, &memory, &handle) == 0, "decoder creation failed"); + std::vector buffer(memory.maxFrameBufferSize); + std::size_t pictures = 0; + const auto take = [&](const OutputInfo& output) { + if (!output.isValid) return false; + check(pictures < PictureHashes.size(), "more pictures than access units"); + check(!output.isErrorFrame && output.frameWidth == Width && output.frameHeight == CodedHeight, "unexpected picture geometry"); + check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference"); + ++pictures; + return true; + }; + for (std::size_t unit = 0; unit < units.size(); ++unit) { + const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), 1000 + unit, unit, 0xa0 + unit}; + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "decode failed"); + take(output); + } + for (;;) { + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Flush_nid_postfix(handle, &frame, &output) == 0, "flush failed"); + if (!take(output)) break; + } + check(pictures == PictureHashes.size(), "missing pictures after flush"); + check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); +} + +bool readsAsOneLengthPrefix(const std::vector& unit) { + const std::size_t length = (static_cast(unit[0]) << 24u) | (static_cast(unit[1]) << 16u) | (static_cast(unit[2]) << 8u) | unit[3]; + return length + 4 == unit.size(); +} + +std::vector lengthPrefixedFiller(std::size_t length) { + std::vector filler; + for (int shift = 24; shift >= 0; shift -= 8) filler.push_back(static_cast(length >> shift)); + filler.push_back(0x0c); + filler.insert(filler.end(), length - 1, 0xff); + return filler; +} + +void testLengthPrefixLooksLikeStartCode() { + auto units = accessUnits(true); + const auto filler = lengthPrefixedFiller(0x101); + check(!(units[0][0] == 0 && units[0][1] == 0 && (units[0][2] == 1 || units[0][3] == 1)), "the first length-prefixed unit already begins with start-code bytes"); + units[1].insert(units[1].begin(), filler.begin(), filler.end()); + check(units[1][0] == 0 && units[1][1] == 0 && units[1][2] == 1, "the crafted unit does not begin with start-code bytes"); + decodeInOrder(units); + + auto mixed = accessUnits(false); + const auto mixedFiller = lengthPrefixedFiller(0x123); + mixed[1] = accessUnits(true)[1]; + mixed[1].insert(mixed[1].begin(), mixedFiller.begin(), mixedFiller.end()); + check(mixed[0][2] == 0 && mixed[0][3] == 1 && mixed[1][0] == 0 && mixed[1][1] == 0 && mixed[1][2] == 1, "the length-prefixed unit does not follow an Annex-B unit with a four-byte start code"); + decodeInOrder(mixed); +} + +void testStartCodeLooksLikeLengthPrefix() { + auto units = accessUnits(false); + for (auto& unit : units) { + check(unit[0] == 0 && unit[1] == 0 && unit[2] == 0 && unit[3] == 1, "an access unit does not open with a four-byte start code"); + unit.erase(unit.begin()); + } + auto& first = units.front(); + std::size_t sequence = 0; + std::size_t afterPicture = 0; + bool picture = false; + for (std::size_t index = 0; index + 3 < first.size(); ++index) { + if (first[index] != 0 || first[index + 1] != 0 || first[index + 2] != 1) continue; + const auto type = first[index + 3] & 0x1fu; + if (picture) { + afterPicture = first[index - 1] == 0 ? index - 1 : index; + break; + } + if (type == 7 && sequence == 0) sequence = index; + if (type == 8 && sequence != 0) picture = true; + index += 2; + } + check(sequence != 0 && afterPicture > sequence, "the first access unit has no SPS and PPS before another NAL"); + const std::vector delimiter(first.begin(), first.begin() + static_cast(first[sequence - 1] == 0 ? sequence - 1 : sequence)); + check(delimiter.size() >= 4 && delimiter[2] == 1 && !readsAsOneLengthPrefix(delimiter), "the access unit delimiter is not an unambiguous unit with a three-byte start code"); + std::vector parameters(first.begin() + static_cast(sequence), first.begin() + static_cast(afterPicture)); + first.erase(first.begin(), first.begin() + static_cast(afterPicture)); + if (first[2] == 0) first.erase(first.begin()); + const std::size_t padded = 4 + ((std::size_t{1} << 8u) | parameters[3]); + check(parameters.size() < padded, "the parameter sets are too long to pad"); + parameters.resize(padded, 0); + check(readsAsOneLengthPrefix(parameters), "the padded parameter sets do not read as one length-prefixed NAL"); + units.insert(units.begin(), {delimiter, parameters}); + decodeInOrder(units); +} + std::uint64_t createDecoder(std::uint32_t codec, std::uint32_t profile) { const DecoderConfigInfo config{sizeof(DecoderConfigInfo), 1, codec, profile, 0, static_cast(Width), static_cast(Height), 4, 1}; DecoderMemoryInfo memory{sizeof(DecoderMemoryInfo)}; @@ -804,6 +901,8 @@ int main() { testFailures(); testDecode(false); testDecode(true); + testLengthPrefixLooksLikeStartCode(); + testStartCodeLooksLikeLengthPrefix(); testDecode(false, true); const auto hevc = hevcUnits(HevcStream.data(), HevcStream.size()); testClip(CodecHevc, 1, hevc, HevcHashes); diff --git a/core/libs/tests/GuestVoice.cpp b/core/libs/tests/GuestVoice.cpp index 98c2b5f187..62d682e6ad 100644 --- a/core/libs/tests/GuestVoice.cpp +++ b/core/libs/tests/GuestVoice.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include extern "C" { @@ -18,6 +19,8 @@ int APS5_VABI sceVoiceGetMuteFlag(std::uint32_t, bool*); int APS5_VABI sceVoiceGetPortInfo(std::uint32_t, VoicePortInfo*); int APS5_VABI sceVoiceReadFromOPort(std::uint32_t, void*, std::uint32_t*); int APS5_VABI sceVoiceResetPort(std::uint32_t); +int APS5_VABI sceVoicePausePort(std::uint32_t); +int APS5_VABI sceVoiceResumePort(std::uint32_t); int APS5_VABI sceVoiceGetResourceInfo(VoiceResourceInfo*); int APS5_VABI sceVoiceEnableChat(void); int APS5_VABI sceVoiceDisableChat(void); @@ -25,6 +28,9 @@ int APS5_VABI sceVoiceDisableChat(void); namespace { +constexpr int NotInitialized = static_cast(0x804E0801u); +constexpr int AlreadyInitialized = static_cast(0x804E0802u); + void Require(bool value) { if (!value) std::abort(); } template @@ -33,6 +39,27 @@ void RequireThrows(TFunction function) { std::abort(); } +void TestNotInitialized() { + Require(sceVoiceStart(nullptr) == NotInitialized); + VoiceStartParam start{}; + Require(sceVoiceStart(&start) == NotInitialized); + Require(sceVoiceCreatePort(nullptr, nullptr) == NotInitialized); + VoicePortParam badType{}; + badType.port_type = 99; + std::uint32_t id = 0xA5A5A5A5u; + Require(sceVoiceCreatePort(&id, &badType) == NotInitialized); + VoicePortParam param{}; + param.port_type = 3; + Require(sceVoiceCreatePort(&id, ¶m) == NotInitialized); + Require(id == 0xA5A5A5A5u); + Require(sceVoiceGetPortInfo(0, nullptr) == NotInitialized); + VoicePortInfo info{}; + std::memset(&info, 0xA5, sizeof(info)); + VoicePortInfo sentinel = info; + Require(sceVoiceGetPortInfo(0, &info) == NotInitialized); + Require(std::memcmp(&info, &sentinel, sizeof(info)) == 0); +} + std::uint32_t CreatePort(std::int32_t type, float volume) { VoicePortParam param{}; param.port_type = type; @@ -46,13 +73,17 @@ std::uint32_t CreatePort(std::int32_t type, float volume) { } // namespace int main() { + TestNotInitialized(); RequireThrows([] { sceVoiceResetPort(0); }); + RequireThrows([] { sceVoicePausePort(0); }); + RequireThrows([] { sceVoiceResumePort(0); }); RequireThrows([] { VoiceResourceInfo info{}; sceVoiceGetResourceInfo(&info); }); RequireThrows([] { sceVoiceEnableChat(); }); RequireThrows([] { sceVoiceDisableChat(); }); VoiceInitParam init{}; VoiceStartParam start{}; Require(sceVoiceInit(&init, 0) == 0); + Require(sceVoiceInit(&init, 0) == AlreadyInitialized); Require(sceVoiceStart(&start) == 0); RequireThrows([] { sceVoiceGetResourceInfo(nullptr); }); VoiceResourceInfo resourceInfo{}; @@ -75,6 +106,16 @@ int main() { Require(sceVoiceResetPort(output) == 0); RequireThrows([&] { sceVoiceResetPort(output + 1); }); + Require(sceVoicePausePort(input) == 0); + Require(sceVoicePausePort(output) == 0); + VoicePortInfo paused{}; + Require(sceVoiceGetPortInfo(input, &paused) == 0); + Require(paused.port_type == before.port_type && paused.state == before.state && paused.frame_size == before.frame_size); + Require(sceVoiceResumePort(input) == 0); + Require(sceVoiceResumePort(output) == 0); + RequireThrows([&] { sceVoicePausePort(output + 1); }); + RequireThrows([&] { sceVoiceResumePort(output + 1); }); + VoicePortInfo after{}; Require(sceVoiceGetPortInfo(input, &after) == 0); Require(after.port_type == before.port_type && after.state == before.state && after.frame_size == before.frame_size); @@ -89,9 +130,12 @@ int main() { Require(sceVoiceDeletePort(input) == 0); RequireThrows([&] { sceVoiceResetPort(input); }); + RequireThrows([&] { sceVoicePausePort(input); }); Require(sceVoiceResetPort(output) == 0); Require(sceVoiceEnd_nid_postfix() == 0); + TestNotInitialized(); RequireThrows([&] { sceVoiceResetPort(output); }); + RequireThrows([&] { sceVoiceResumePort(output); }); RequireThrows([] { VoiceResourceInfo info{}; sceVoiceGetResourceInfo(&info); }); RequireThrows([] { sceVoiceEnableChat(); }); RequireThrows([] { sceVoiceDisableChat(); }); diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index abbba3196d..809d47fe3e 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -322,12 +322,12 @@ int main() { const char16_t* half = u"-1.5é"; require(wcstold_nid_postfix(half, &end) == -1.5L && end == half + 4); const char16_t* wideNan = u" -nan(0x123)é"; - require(std::bit_cast(wcstod_nid_postfix(wideNan, &end)) == 0xfff8000000000000u && end == wideNan + 12); - require(std::bit_cast(wcstof_nid_postfix(wideNan, &end)) == 0xffc00000u && end == wideNan + 12); + require(std::bit_cast(wcstod_nid_postfix(wideNan, &end)) == 0xfff8000000000123u && end == wideNan + 12); + require(std::bit_cast(wcstof_nid_postfix(wideNan, &end)) == 0x7fc00000u && end == wideNan + 12); const long double wideExtended = wcstold_nid_postfix(wideNan, &end); std::uint64_t significand; std::memcpy(&significand, &wideExtended, sizeof(significand)); - require(std::isnan(wideExtended) && std::signbit(wideExtended) && significand == 0xc000000000000000u && end == wideNan + 12); + require(std::isnan(wideExtended) && !std::signbit(wideExtended) && significand == 0xc000000000000000u && end == wideNan + 12); require(wcscoll_nid_postfix(u"a", u"b") < 0); require(wcscoll_nid_postfix(u"ï¿¿", u"a") > 0); diff --git a/core/libs/tests/JsonCopyFailure.cpp b/core/libs/tests/JsonCopyFailure.cpp index a3c64622f2..0fa28bad98 100644 --- a/core/libs/tests/JsonCopyFailure.cpp +++ b/core/libs/tests/JsonCopyFailure.cpp @@ -24,6 +24,8 @@ void APS5_VABI _ZN3sce4Json5ValueD1Ev(Value*); Value* APS5_VABI _ZN3sce4Json5ValueaSERKS1_(Value*, const Value*); int APS5_VABI _ZN3sce4Json5Value3setERKNS0_5ArrayE(Value*, const Array*); int APS5_VABI _ZN3sce4Json5Value3setERKNS0_6ObjectE(Value*, const Object*); +int APS5_VABI _ZN3sce4Json5Value3setENS0_9ValueTypeE(Value*, std::int32_t); +std::int32_t APS5_VABI _ZNK3sce4Json5Value7getTypeEv(const Value*); const Array* APS5_VABI _ZNK3sce4Json5Value8getArrayEv(const Value*); const Object* APS5_VABI _ZNK3sce4Json5Value9getObjectEv(const Value*); int APS5_VABI _ZN3sce4Json5Value9serializeERNS0_6StringE(Value*, String*); @@ -92,7 +94,7 @@ static void CheckValue(const Value& value, const char* expected) { enum class Operation { ValueCopy, ArrayCopy, ObjectCopy, ValueAssign, ObjectAssign, ArrayAppend, ValueArray, ValueObject, ValueString, ValueText, ValueTypeString, ValueTypeArray, - ValueTypeObject, SetArray, SetObject, PairCopy + ValueTypeObject, SetArray, SetObject, SetTypeArray, SetTypeObject, SetTypeString, PairCopy }; static bool Run(Operation operation, const Value& source, const char* expected, const Array& array, const Object& object, const String& text) { @@ -101,7 +103,8 @@ static bool Run(Operation operation, const Value& source, const char* expected, Object objectResult{}; Pair pair{}; bool valueReady = false, arrayReady = false, objectReady = false, pairReady = false; - if (operation == Operation::ValueAssign || operation == Operation::SetArray || operation == Operation::SetObject) { + const bool typeSetter = operation == Operation::SetTypeArray || operation == Operation::SetTypeObject || operation == Operation::SetTypeString; + if (operation == Operation::ValueAssign || operation == Operation::SetArray || operation == Operation::SetObject || typeSetter) { _ZN3sce4Json5ValueC1EPKc(&result, "previous"); valueReady = true; } @@ -126,6 +129,9 @@ static bool Run(Operation operation, const Value& source, const char* expected, case Operation::ValueTypeObject: _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(&result, 7); valueReady = true; break; case Operation::SetArray: _ZN3sce4Json5Value3setERKNS0_5ArrayE(&result, &array); break; case Operation::SetObject: _ZN3sce4Json5Value3setERKNS0_6ObjectE(&result, &object); break; + case Operation::SetTypeArray: _ZN3sce4Json5Value3setENS0_9ValueTypeE(&result, 6); break; + case Operation::SetTypeObject: _ZN3sce4Json5Value3setENS0_9ValueTypeE(&result, 7); break; + case Operation::SetTypeString: _ZN3sce4Json5Value3setENS0_9ValueTypeE(&result, 5); break; case Operation::PairCopy: _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&pair, &text, &source); pairReady = true; break; } succeeded = true; @@ -134,7 +140,10 @@ static bool Run(Operation operation, const Value& source, const char* expected, allocationBudget = -1; if (valueReady) { if (succeeded) CheckValue(result, expected); - else CheckValue(result, "\"previous\""); + else { + Require(_ZNK3sce4Json5Value7getTypeEv(&result) == 5, "failed setter changed the destination type"); + CheckValue(result, "\"previous\""); + } _ZN3sce4Json5ValueD1Ev(&result); } if (arrayReady) { @@ -183,6 +192,9 @@ int main() { {"empty object value", Operation::ValueTypeObject, &objectValue, "{}"}, {"set array", Operation::SetArray, &arrayValue, arrayJson}, {"set object", Operation::SetObject, &objectValue, objectJson}, + {"set empty array", Operation::SetTypeArray, &objectValue, "[]"}, + {"set empty object", Operation::SetTypeObject, &objectValue, "{}"}, + {"set empty string", Operation::SetTypeString, &objectValue, "\"\""}, {"pair copy", Operation::PairCopy, &objectValue, objectJson}, }; unsigned failures = 0; diff --git a/core/libs/tests/ParamJsonParser.cpp b/core/libs/tests/ParamJsonParser.cpp index 8d9a2ffbd9..8ec7173a5a 100644 --- a/core/libs/tests/ParamJsonParser.cpp +++ b/core/libs/tests/ParamJsonParser.cpp @@ -81,6 +81,12 @@ static void CheckUserDefinedParams(const std::filesystem::path& path) { } } +static void CheckContentVersion(const std::filesystem::path& path) { + Require(ParseText(path, R"({"titleId":"PPSA00000","contentVersion":"01.002.000","localizedParameters":{"en-US":{"titleName":"Example"}}})").contentVersion == "01.002.000", "Incorrect content version"); + Require(ParseText(path, R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}}})").contentVersion.empty(), "A missing content version was not empty"); + Require(ParseText(path, R"({"titleId":"PPSA00000","contentVersion":1,"localizedParameters":{"en-US":{"titleName":"Example"}}})").contentVersion.empty(), "A content version that is not a string was kept"); +} + static void CheckNesting(const std::filesystem::path& path) { const std::string prefix = R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}},"extra":)"; const auto nested = [&](std::size_t arrays) { @@ -226,6 +232,7 @@ int main() { } CheckLanguages(path); CheckUserDefinedParams(path); + CheckContentVersion(path); CheckOutputPermission(path); CheckNesting(path); } catch (const std::exception& error) { diff --git a/core/libs/tests/SaveDataFileWriteFailure.cpp b/core/libs/tests/SaveDataFileWriteFailure.cpp index d9046fb753..933392c62a 100644 --- a/core/libs/tests/SaveDataFileWriteFailure.cpp +++ b/core/libs/tests/SaveDataFileWriteFailure.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -17,46 +18,160 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) +static std::vector ReadFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + Require(file.is_open()); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +static void WriteFile(const std::filesystem::path& path, const std::vector& data) { + std::ofstream file(path, std::ios::binary); + Require(file.is_open()); + file.write(data.data(), static_cast(data.size())); + Require(static_cast(file)); +} + +static bool ReplaceFile(const std::filesystem::path& temporary, const std::filesystem::path& destination) { +#ifdef _WIN32 + return MoveFileExW(temporary.c_str(), destination.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0; +#else + std::error_code error; + std::filesystem::rename(temporary, destination, error); + return !error; +#endif +} + int main() { const auto root = std::filesystem::temp_directory_path() / ("anyps5-savedata-write-failure-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); - const auto savePath = root / "memory.dat"; + const auto savePath = root / std::filesystem::path(u8"m\u00e9moire-\u65e5\u672c.dat"); const std::vector oldData{'o', 'l', 'd'}; const std::vector replacement{'n', 'e', 'w'}; - { - std::ofstream file(savePath, std::ios::binary); - Require(file.is_open()); - file.write(oldData.data(), static_cast(oldData.size())); - Require(static_cast(file)); - } + WriteFile(savePath, oldData); int replaceAttempts = 0; + std::filesystem::path failedTemporary; const bool replaced = savedata::replace_file_with( savePath, replacement.data(), replacement.size(), - [&replaceAttempts, &savePath](const std::filesystem::path& temporary, - const std::filesystem::path& destination) { + [&](const std::filesystem::path& temporary, const std::filesystem::path& destination) { ++replaceAttempts; Require(destination == savePath); - auto expectedTemporary = savePath; - expectedTemporary += ".tmp"; - Require(temporary == expectedTemporary); + Require(temporary != destination && temporary.parent_path() == destination.parent_path()); + Require(ReadFile(temporary) == replacement); + failedTemporary = temporary; return false; }); Require(!replaced); Require(replaceAttempts == 1); - std::vector savedData; - { - std::ifstream savedFile(savePath, std::ios::binary); - savedData.assign(std::istreambuf_iterator(savedFile), std::istreambuf_iterator()); - } - Require(savedData == oldData); + Require(ReadFile(savePath) == oldData); + Require(!std::filesystem::exists(failedTemporary)); + auto temporary = savePath; temporary += ".tmp"; - Require(!std::filesystem::exists(temporary)); + WriteFile(temporary, oldData); + Require(savedata::replace_file(savePath, replacement.data(), replacement.size())); + Require(ReadFile(savePath) == replacement); + Require(std::filesystem::exists(temporary)); + Require(ReadFile(temporary) == oldData); + std::filesystem::remove(temporary); + + const auto unrelated = root / "unrelated.dat"; + WriteFile(unrelated, oldData); + std::filesystem::create_hard_link(unrelated, temporary); + Require(savedata::replace_file(savePath, replacement.data(), replacement.size())); + Require(ReadFile(unrelated) == oldData); + Require(std::filesystem::equivalent(unrelated, temporary)); + Require(!std::filesystem::equivalent(unrelated, savePath)); + std::filesystem::remove(temporary); + + std::error_code symlinkError; +#ifdef _WIN32 + if (!CreateSymbolicLinkW(temporary.c_str(), unrelated.c_str(), SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE)) { + symlinkError = std::error_code(GetLastError(), std::system_category()); + } +#else + std::filesystem::create_symlink(unrelated, temporary, symlinkError); +#endif +#ifdef _WIN32 + if (symlinkError == std::errc::permission_denied || + symlinkError == std::error_code(ERROR_PRIVILEGE_NOT_HELD, std::system_category())) { + std::printf("Symlink case skipped: creating Windows symlinks requires developer mode or privilege\n"); + } else +#endif + { + Require(!symlinkError); + Require(savedata::replace_file(savePath, replacement.data(), replacement.size())); + Require(ReadFile(unrelated) == oldData); +#ifdef _WIN32 + const auto linkAttributes = GetFileAttributesW(temporary.c_str()); + const auto saveAttributes = GetFileAttributesW(savePath.c_str()); + Require(linkAttributes != INVALID_FILE_ATTRIBUTES && (linkAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0); + Require(saveAttributes != INVALID_FILE_ATTRIBUTES && (saveAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0); +#else + Require(std::filesystem::is_symlink(temporary)); + Require(!std::filesystem::is_symlink(savePath)); +#endif + Require(ReadFile(savePath) == replacement); + std::filesystem::remove(temporary); + } + + Require(std::filesystem::create_directory(temporary)); + Require(savedata::replace_file(savePath, replacement.data(), replacement.size())); + Require(std::filesystem::is_directory(temporary)); + std::filesystem::remove(temporary); + + std::filesystem::path outerTemporary; + Require(savedata::replace_file_with(savePath, replacement.data(), replacement.size(), + [&](const auto& outer, const auto& destination) { + outerTemporary = outer; + Require(savedata::replace_file_with(savePath, oldData.data(), oldData.size(), + [&](const auto& inner, const auto& innerDestination) { + Require(inner != outer); + Require(ReadFile(outer) == replacement); + Require(ReadFile(inner) == oldData); + return ReplaceFile(inner, innerDestination); + })); + Require(ReadFile(destination) == oldData); + Require(ReadFile(outer) == replacement); + return ReplaceFile(outer, destination); + })); + Require(ReadFile(savePath) == replacement); + Require(!std::filesystem::exists(outerTemporary)); + + Require(savedata::replace_file_with(savePath, replacement.data(), replacement.size(), + [&](const auto& created, const auto& destination) { + Require(ReplaceFile(created, destination)); + outerTemporary = created; + WriteFile(created, oldData); + return true; + })); + Require(ReadFile(outerTemporary) == oldData); + std::filesystem::remove(outerTemporary); + + std::filesystem::path thrownTemporary; + bool threw = false; + try { + savedata::replace_file_with(savePath, oldData.data(), oldData.size(), + [&](const auto& created, const auto&) -> bool { + thrownTemporary = created; + throw std::runtime_error("injected replacement failure"); + }); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && !thrownTemporary.empty()); + Require(!std::filesystem::exists(thrownTemporary)); + Require(ReadFile(savePath) == replacement); + + Require(!savedata::replace_file(savePath, nullptr, 1)); + Require(ReadFile(savePath) == replacement); + Require(savedata::replace_file(savePath, nullptr, 0)); + Require(ReadFile(savePath).empty()); + Require(std::distance(std::filesystem::directory_iterator(root), std::filesystem::directory_iterator{}) == 2); std::filesystem::remove_all(root); } diff --git a/core/libs/tests/SaveDataMemoryJournal.cpp b/core/libs/tests/SaveDataMemoryJournal.cpp index 89c0d79a82..e661a1fd1f 100644 --- a/core/libs/tests/SaveDataMemoryJournal.cpp +++ b/core/libs/tests/SaveDataMemoryJournal.cpp @@ -91,10 +91,16 @@ int main() { Require(std::filesystem::create_directory(temporaryPath)); first.fill('q'); - Require(sceSaveDataSetSaveDataMemory2(&set) == static_cast(0x809F000Bu)); - Require(Read(memoryPath) == expected); + Require(sceSaveDataSetSaveDataMemory2(&set) == 0); + std::copy(first.begin(), first.end(), expected.begin() + 8); + Require(Read(memoryPath) == expected && Read(aliasPath) == expected); Require(!std::filesystem::exists(journalPath)); - Require(!std::filesystem::exists(temporaryPath)); + Require(std::filesystem::is_directory(temporaryPath)); + Require(std::filesystem::remove(temporaryPath)); + const auto temporaryPrefix = temporaryPath.filename().string(); + for (const auto& entry : std::filesystem::directory_iterator(memoryPath.parent_path())) { + Require(!entry.path().filename().string().starts_with(temporaryPrefix)); + } Write(journalPath, Journal(16, "jour")); std::array got{}; diff --git a/core/libs/tests/SaveDataMemoryMetadata.cpp b/core/libs/tests/SaveDataMemoryMetadata.cpp index b3ba4f10ef..b3acb20559 100644 --- a/core/libs/tests/SaveDataMemoryMetadata.cpp +++ b/core/libs/tests/SaveDataMemoryMetadata.cpp @@ -12,7 +12,15 @@ #include #include #include -#ifndef _WIN32 +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#else #include #include #endif @@ -45,6 +53,48 @@ static std::vector Read(const std::filesystem::path& path) { return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; } +static void RequireNoTemporary(const std::filesystem::path& path) { + const auto prefix = path.filename().string() + ".tmp"; + for (const auto& entry : std::filesystem::directory_iterator(path.parent_path())) { + Require(!entry.path().filename().string().starts_with(prefix)); + } +} + +template +static int BlockDestination(const std::filesystem::path& path, Operation&& operation) { + Require(std::filesystem::create_directory(path)); + const int result = operation(); + Require(std::filesystem::is_directory(path)); + Require(std::filesystem::remove(path)); + RequireNoTemporary(path); + return result; +} + +template +static int FailWrite(const std::filesystem::path& path, [[maybe_unused]] std::size_t sizeLimit, + Operation&& operation) { +#ifdef _WIN32 + const auto locked = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + Require(locked != INVALID_HANDLE_VALUE); + const int result = operation(); + Require(CloseHandle(locked) != 0); +#else + rlimit previousLimit{}; + Require(getrlimit(RLIMIT_FSIZE, &previousLimit) == 0); + auto writeLimit = previousLimit; + writeLimit.rlim_cur = sizeLimit; + const auto previousHandler = std::signal(SIGXFSZ, SIG_IGN); + Require(previousHandler != SIG_ERR); + Require(setrlimit(RLIMIT_FSIZE, &writeLimit) == 0); + const int result = operation(); + Require(setrlimit(RLIMIT_FSIZE, &previousLimit) == 0); + Require(std::signal(SIGXFSZ, previousHandler) != SIG_ERR); +#endif + RequireNoTemporary(path); + return result; +} + int main() { Require(sceSaveDataCreateTransactionResource(1024) == kSaveDataNotInitialized); Require(sceSaveDataDeleteTransactionResource(1) == kSaveDataNotInitialized); @@ -82,12 +132,9 @@ int main() { const auto original = Read(path); Require(original.size() == sizeof(param)); param.user_param = 42; - const auto temporary = path.string() + ".tmp"; - Require(std::filesystem::create_directory(temporary)); - const int status = sceSaveDataSetSaveDataMemory2(&set); + const int status = FailWrite(path, 16, [&] { return sceSaveDataSetSaveDataMemory2(&set); }); Require(Read(path) == original); Require(status == static_cast(0x809F000Bu)); - Require(!std::filesystem::exists(temporary)); Require(sceSaveDataSetSaveDataMemory2(&set) == 0); const auto* bytes = reinterpret_cast(¶m); Require(Read(path) == std::vector(bytes, bytes + sizeof(param))); @@ -106,26 +153,9 @@ int main() { Require(sceSaveDataSetSaveDataMemory2(&set) == static_cast(0x809F0000u)); Require(Read(memoryPath) == memoryOriginal); data[1].offset = 24; - const auto memoryTemporary = memoryPath.string() + ".tmp"; - Require(std::filesystem::create_directory(memoryTemporary)); - Require(sceSaveDataSetSaveDataMemory2(&set) == static_cast(0x809F000Bu)); - Require(Read(memoryPath) == memoryOriginal); - Require(!std::filesystem::exists(memoryTemporary)); -#ifndef _WIN32 - rlimit previousLimit{}; - Require(getrlimit(RLIMIT_FSIZE, &previousLimit) == 0); - auto writeLimit = previousLimit; - writeLimit.rlim_cur = 16; - const auto previousHandler = std::signal(SIGXFSZ, SIG_IGN); - Require(previousHandler != SIG_ERR); - Require(setrlimit(RLIMIT_FSIZE, &writeLimit) == 0); - const int writeStatus = sceSaveDataSetSaveDataMemory2(&set); - Require(setrlimit(RLIMIT_FSIZE, &previousLimit) == 0); - Require(std::signal(SIGXFSZ, previousHandler) != SIG_ERR); + const int writeStatus = FailWrite(memoryPath, 16, [&] { return sceSaveDataSetSaveDataMemory2(&set); }); Require(writeStatus == static_cast(0x809F000Bu)); Require(Read(memoryPath) == memoryOriginal); - Require(!std::filesystem::exists(memoryTemporary)); -#endif Require(sceSaveDataSetSaveDataMemory2(&set) == 0); auto memoryExpected = memoryOriginal; std::copy(first.begin(), first.end(), memoryExpected.begin() + 4); @@ -156,9 +186,7 @@ int main() { Require(Read(memoryPath) == memoryExpected); const auto setupParamPath = std::filesystem::path("_sd_mem/u42/slot0.param"); const auto setupBinPath = std::filesystem::path("_sd_mem/u42/slot0.bin"); - const auto setupParamTemp = setupParamPath.string() + ".tmp"; Require(std::filesystem::create_directories(setupParamPath.parent_path())); - Require(std::filesystem::create_directory(setupParamTemp)); SaveDataParam setupParam{}; setupParam.user_param = 100; SaveDataMemorySetup2 setup2{}; @@ -169,11 +197,10 @@ int main() { setup2.init_param = &setupParam; SaveDataMemorySetupResult setupResult{}; setupResult.existed_memory_size = 555; - Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); + Require(BlockDestination(setupParamPath, [&] { return sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult); }) == static_cast(0x809F000Bu)); Require(setupResult.existed_memory_size == 555); Require(!std::filesystem::exists(setupBinPath)); Require(!std::filesystem::exists(setupParamPath)); - Require(!std::filesystem::exists(setupParamTemp)); Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); Require(setupResult.existed_memory_size == 0); Require(Read(setupBinPath) == std::vector(128, 0)); @@ -188,18 +215,15 @@ int main() { seededFile.write(seededBin.data(), static_cast(seededBin.size())); Require(static_cast(seededFile)); } - const auto growthParamTemp = setupParamPath.string() + ".tmp"; - Require(std::filesystem::create_directory(growthParamTemp)); SaveDataParam previousParam = setupParam; setupParam.user_param = 200; setup2.memory_size = 256; setupResult.existed_memory_size = 999; - Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); + Require(FailWrite(setupParamPath, 16, [&] { return sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult); }) == static_cast(0x809F000Bu)); Require(setupResult.existed_memory_size == 999); Require(Read(setupBinPath) == seededBin); const auto* previousBytes = reinterpret_cast(&previousParam); Require(Read(setupParamPath) == std::vector(previousBytes, previousBytes + sizeof(previousParam))); - Require(!std::filesystem::exists(growthParamTemp)); Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); Require(setupResult.existed_memory_size == 128); std::vector expectedGrownBin = seededBin; @@ -207,28 +231,24 @@ int main() { Require(Read(setupBinPath) == expectedGrownBin); const auto* updatedBytes = reinterpret_cast(&setupParam); Require(Read(setupParamPath) == std::vector(updatedBytes, updatedBytes + sizeof(setupParam))); - const auto growthBinTemp = setupBinPath.string() + ".tmp"; - Require(std::filesystem::create_directory(growthBinTemp)); setupParam.user_param = 300; - setup2.memory_size = 512; + constexpr std::size_t GrownMemorySize = 2 * sizeof(SaveDataParam); + setup2.memory_size = GrownMemorySize; setupResult.existed_memory_size = 888; - Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); + Require(FailWrite(setupBinPath, sizeof(SaveDataParam), [&] { return sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult); }) == static_cast(0x809F000Bu)); Require(setupResult.existed_memory_size == 888); Require(Read(setupBinPath) == expectedGrownBin); const auto* param300Bytes = reinterpret_cast(&setupParam); Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); - Require(!std::filesystem::exists(growthBinTemp)); Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); Require(setupResult.existed_memory_size == 256); - std::vector expectedBlob512 = expectedGrownBin; - expectedBlob512.resize(512, 0); - Require(Read(setupBinPath) == expectedBlob512); + std::vector expectedFinalBlob = expectedGrownBin; + expectedFinalBlob.resize(GrownMemorySize, 0); + Require(Read(setupBinPath) == expectedFinalBlob); Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); const auto freshFailBinPath = std::filesystem::path("_sd_mem/u55/slot0.bin"); const auto freshFailParamPath = std::filesystem::path("_sd_mem/u55/slot0.param"); Require(std::filesystem::create_directories(freshFailBinPath.parent_path())); - const auto freshFailBinTemp = freshFailBinPath.string() + ".tmp"; - Require(std::filesystem::create_directory(freshFailBinTemp)); SaveDataParam freshFailParam{}; freshFailParam.user_param = 55; SaveDataMemorySetup2 freshFailSetup{}; @@ -239,12 +259,11 @@ int main() { freshFailSetup.init_param = &freshFailParam; SaveDataMemorySetupResult freshFailResult{}; freshFailResult.existed_memory_size = 777; - Require(sceSaveDataSetupSaveDataMemory2(&freshFailSetup, &freshFailResult) == static_cast(0x809F000Bu)); + Require(BlockDestination(freshFailBinPath, [&] { return sceSaveDataSetupSaveDataMemory2(&freshFailSetup, &freshFailResult); }) == static_cast(0x809F000Bu)); Require(freshFailResult.existed_memory_size == 777); Require(!std::filesystem::exists(freshFailBinPath)); const auto* freshFailBytes = reinterpret_cast(&freshFailParam); Require(Read(freshFailParamPath) == std::vector(freshFailBytes, freshFailBytes + sizeof(freshFailParam))); - Require(!std::filesystem::exists(freshFailBinTemp)); Require(sceSaveDataSetupSaveDataMemory2(&freshFailSetup, &freshFailResult) == 0); Require(freshFailResult.existed_memory_size == 0); Require(Read(freshFailBinPath) == std::vector(64, 0)); @@ -316,13 +335,13 @@ int main() { const auto* orphanBytes = reinterpret_cast(&orphanParam); Require(Read(orphanParamPath) == std::vector(orphanBytes, orphanBytes + sizeof(orphanParam))); Require(sceSaveDataTerminate() == 0); - Require(Read(setupBinPath) == expectedBlob512); + Require(Read(setupBinPath) == expectedFinalBlob); Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); Require(sceSaveDataInitialize3(nullptr) == 0); setupResult.existed_memory_size = 111; Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); - Require(setupResult.existed_memory_size == 512); - Require(Read(setupBinPath) == expectedBlob512); + Require(setupResult.existed_memory_size == GrownMemorySize); + Require(Read(setupBinPath) == expectedFinalBlob); Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); Require(sceSaveDataTerminate() == 0); Require(sceSaveDataCreateTransactionResource(1024) == kSaveDataNotInitialized); diff --git a/core/libs/tests/SslCertificateFixture.hpp b/core/libs/tests/SslCertificateFixture.hpp index bc5637db37..0c86103e10 100644 --- a/core/libs/tests/SslCertificateFixture.hpp +++ b/core/libs/tests/SslCertificateFixture.hpp @@ -91,3 +91,41 @@ g+KCUQ46CTPPM2bFyypaJZ3ZJq2hRANCAASOWLxgY2XcCm5WcZKzafvQHUuJX1Kt bMsNIbMUyke+Sp9+LrTGU52se7eyb3nq5AVuGiBdmoW5QkBMFx00hkyB -----END PRIVATE KEY----- )FIXTURE"; +inline constexpr char ExpiredServerPem[] = R"FIXTURE(-----BEGIN CERTIFICATE----- +MIIBxjCCAWygAwIBAgICEAMwCgYIKoZIzj0EAwIwQjELMAkGA1UEBhMCR0IxFTAT +BgNVBAoMDEFueVBTNSBUZXN0czEcMBoGA1UEAwwTQW55UFM1IGZpeHR1cmUgcm9v +dDAeFw0yNTA5MTQwMDAwMDBaFw0yNjA5MTkwMDAwMDBaMDgxCzAJBgNVBAYTAkdC +MRUwEwYDVQQKDAxBbnlQUzUgVGVzdHMxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABDvmM3W4JGu6KyrQLp/qYYvTX4MBUwWGIP9+ +VcRgPCO7jwtb0Aoaf6tSaG1Reax8J2rvTRRDRibTwZhg/6a7LKSjXDBaMBQGA1Ud +EQQNMAuCCWxvY2FsaG9zdDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUF +BwMBMB8GA1UdIwQYMBaAFFZolbTcGqYVMtT6BD37g9yqaAzcMAoGCCqGSM49BAMC +A0gAMEUCIFOAMPrcpBrmsJrbXaWzyCDe1UweszENyWAW4pZqs/hmAiEA/vehYXPI +exj4IIRU7Zt80wBXEjJNBk8KXmhO/UvPh/A= +-----END CERTIFICATE----- +)FIXTURE"; +inline constexpr char ExpiredServerKey[] = R"FIXTURE(-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg4TeyA0YK3Bq0Rt2l +On7kv/wcghGMcWvA4f5xVsGJUcahRANCAAQ75jN1uCRruisq0C6f6mGL01+DAVMF +hiD/flXEYDwju48LW9AKGn+rUmhtUXmsfCdq700UQ0Ym08GYYP+muyyk +-----END PRIVATE KEY----- +)FIXTURE"; +inline constexpr char FutureServerPem[] = R"FIXTURE(-----BEGIN CERTIFICATE----- +MIIBxzCCAWygAwIBAgICEAQwCgYIKoZIzj0EAwIwQjELMAkGA1UEBhMCR0IxFTAT +BgNVBAoMDEFueVBTNSBUZXN0czEcMBoGA1UEAwwTQW55UFM1IGZpeHR1cmUgcm9v +dDAeFw0zNzExMjMwMDAwMDBaFw0zODEyMjcwMDAwMDBaMDgxCzAJBgNVBAYTAkdC +MRUwEwYDVQQKDAxBbnlQUzUgVGVzdHMxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABP1SqW8j3y1k1uDBPD2cFW3vDPOb09zGYm6g +mUpt2UXlx6WTsiohSzv9emkUSe/YJez3mLoEB4c/ohL0rNhdGTqjXDBaMBQGA1Ud +EQQNMAuCCWxvY2FsaG9zdDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUF +BwMBMB8GA1UdIwQYMBaAFFZolbTcGqYVMtT6BD37g9yqaAzcMAoGCCqGSM49BAMC +A0kAMEYCIQDG0pZG6c9W7tVIA9AoG8YVyQbBZ0krnjw4SlAfy1I3swIhAMIqV5JD +tjTwhuPqzDgkmObSwb6GGD1abVRLCDJJMjsZ +-----END CERTIFICATE----- +)FIXTURE"; +inline constexpr char FutureServerKey[] = R"FIXTURE(-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgecaFHjyMpE80Z1ja +I7gmZUa8yQKqbgS9PZq3PQPD3h+hRANCAAT9UqlvI98tZNbgwTw9nBVt7wzzm9Pc +xmJuoJlKbdlF5celk7IqIUs7/XppFEnv2CXs95i6BAeHP6IS9KzYXRk6 +-----END PRIVATE KEY----- +)FIXTURE"; diff --git a/core/libs/tests/WindowsExceptions.cpp b/core/libs/tests/WindowsExceptions.cpp index d7adc8c92e..c83acd49c2 100644 --- a/core/libs/tests/WindowsExceptions.cpp +++ b/core/libs/tests/WindowsExceptions.cpp @@ -151,6 +151,26 @@ static void testGuestAbortReport() { throw std::runtime_error(std::string("guest abort child exited with ") + code + " and printed:\n" + output); } +[[gnu::noinline, noreturn]] static void GuestAssertProbe() { + __asm__ volatile("int $0x41"); + __builtin_unreachable(); +} + +static int RunAssertChild() { + std::fprintf(stderr, "assert probe\n"); + std::fflush(stderr); + GuestAssertProbe(); +} + +static void testGuestAssertReport() { + DWORD exitCode = 0; + const std::string output = RunChild(L"\" guest-assert", exitCode); + char code[16]; + std::snprintf(code, sizeof(code), "0x%08lx", static_cast(exitCode)); + if (exitCode != 0xC0000005 || output.find("guest assert/break (int 0x41)") == std::string::npos) + throw std::runtime_error(std::string("guest assert child exited with ") + code + " and printed:\n" + output); +} + static int destroyed; struct Guard { ~Guard() { ++destroyed; } }; static void ThrowNested() { @@ -299,11 +319,13 @@ int main(int argc, char** argv) { if (argc > 1) { if (std::strcmp(argv[1], "throw-uncaught") == 0) return RunThrowChild(); if (std::strcmp(argv[1], "guest-abort") == 0) return RunAbortChild(); + if (std::strcmp(argv[1], "guest-assert") == 0) return RunAssertChild(); return 100; } TestTypeInfoVtables(); testUnhandledThrowSite(); testGuestAbortReport(); + testGuestAssertReport(); testExceptionPointer(); testFutureException(); testMadeExceptionPointer(); diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index d2a3387683..a3b0cc7e5c 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -457,6 +457,9 @@ if(BUILD_TESTING) add_test(NAME guest_relative_section_symbols COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_relative_section_symbols.py $) set_tests_properties(guest_relative_section_symbols PROPERTIES TIMEOUT 20) + add_test(NAME overlapping_relocations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_overlapping_relocations.py $) + set_tests_properties(overlapping_relocations PROPERTIES TIMEOUT 30) add_test(NAME optional_plt COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) add_test(NAME optional_rela COMMAND ${Python3_EXECUTABLE} @@ -499,6 +502,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_executable_exports.py $) add_test(NAME executable_tls_relocations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executable_tls_relocations.py $) + add_test(NAME macos_empty_tls COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_empty_tls.py $) + set_tests_properties(macos_empty_tls PROPERTIES TIMEOUT 60) add_test(NAME linux_entry_argv COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_entry_argv.py $) add_test(NAME linux_fixed_base COMMAND ${Python3_EXECUTABLE} @@ -583,6 +589,10 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_absolute_symbols.py $) add_test(NAME guest_module_identity COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_identity.py $) + add_test(NAME guest_unused_import_dependencies COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_unused_import_dependencies.py + $ $) + set_property(TEST guest_unused_import_dependencies APPEND PROPERTY LABELS relinker) add_test(NAME guest_export_lookup COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_export_lookup.py $) set_tests_properties(guest_export_lookup PROPERTIES TIMEOUT 30) @@ -662,6 +672,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_relro.py $) add_test(NAME sce_module_code_extent COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_module_code_extent.py $) + add_test(NAME sce_code_gaps COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_code_gaps.py $) add_test(NAME call_registry COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_call_registry.py $) add_test(NAME jmprel_table_bounds COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/codegen/include/codegen/IInstructionScanner.hpp b/core/relinker/codegen/include/codegen/IInstructionScanner.hpp index 2ea39a4e4f..e5314b276a 100644 --- a/core/relinker/codegen/include/codegen/IInstructionScanner.hpp +++ b/core/relinker/codegen/include/codegen/IInstructionScanner.hpp @@ -17,7 +17,7 @@ public: virtual void VisitCodeSection(const std::vector& codeSection, Domain::FileByteOffset codeSectionOffset, Domain::FileByteOffset codeSectionSize, const std::function& visit) const = 0; }; -std::unique_ptr MakeInstructionScanner(std::function progress = {}); +std::unique_ptr MakeInstructionScanner(std::function progress = {}, std::vector unwindRanges = {}); } diff --git a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp index f82d8a19f9..c3df3388cc 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp @@ -65,6 +65,7 @@ inline constexpr Entry kSha1msg2 = {"SHA1MSG2", nullptr, 0}; inline constexpr std::uint8_t kMovsStoreOpcode = 0x11; inline constexpr std::uint8_t kPshufbZero = 0x80; inline constexpr std::uint8_t kTrapFill = 0xCC; +inline constexpr std::uint8_t kRdpruTrampolineBytes[] = {0x9C, 0x85, 0xC9, 0x74, 0x0B, 0x83, 0xF9, 0x01, 0x74, 0x06, 0x31, 0xC0, 0x31, 0xD2, 0xEB, 0x02, 0x0F, 0x31, 0x9D}; inline constexpr std::size_t kStubAlignment = 16; inline constexpr std::size_t kRedZoneSpillFrame = 0x90; diff --git a/core/relinker/codegen/src/InstructionScanner.cpp b/core/relinker/codegen/src/InstructionScanner.cpp index 328e1e532b..6a6c0018b1 100644 --- a/core/relinker/codegen/src/InstructionScanner.cpp +++ b/core/relinker/codegen/src/InstructionScanner.cpp @@ -8,28 +8,50 @@ namespace Codegen { +namespace { + +std::vector MergeCodeRanges(std::vector ranges) { + std::erase_if(ranges, [](const Domain::CodeRange& range) { return range.End <= range.Begin; }); + std::sort(ranges.begin(), ranges.end(), [](const Domain::CodeRange& left, const Domain::CodeRange& right) { return left.Begin < right.Begin; }); + std::vector merged; + for (const auto& range : ranges) { + if (!merged.empty() && range.Begin <= merged.back().End) + merged.back().End = std::max(merged.back().End, range.End); + else + merged.push_back(range); + } + return merged; +} + +} + class InstructionScanner : public IInstructionScanner { public: - explicit InstructionScanner(std::function progress) : progress(std::move(progress)) {} + InstructionScanner(std::function progress, std::vector unwindRanges) : progress(std::move(progress)), unwindRanges(MergeCodeRanges(std::move(unwindRanges))) {} [[nodiscard]] std::vector ScanCodeSection(const std::vector& codeSection, Domain::FileByteOffset codeSectionOffset, Domain::FileByteOffset codeSectionSize) const override; void VisitCodeSection(const std::vector& codeSection, Domain::FileByteOffset codeSectionOffset, Domain::FileByteOffset codeSectionSize, const std::function& visit) const override; private: std::function progress; + std::vector unwindRanges; }; template -void ScanCodeSection(const std::vector& codeSection, const Domain::FileByteOffset codeSectionOffset, const Domain::FileByteOffset codeSectionSize, const std::function& progress, TVisitor&& visit) { +void ScanCodeSection(const std::vector& codeSection, const Domain::FileByteOffset codeSectionOffset, const Domain::FileByteOffset codeSectionSize, const std::function& progress, const std::vector& unwindRanges, TVisitor&& visit) { if (codeSectionSize > codeSection.size()) throw CodegenException("Code section exceeds the input buffer", codeSectionOffset); if (codeSectionSize > std::numeric_limits::max() - codeSectionOffset) throw CodegenException("Code section offset overflow", codeSectionOffset); const auto limit = static_cast(codeSectionSize); const X64InstructionDecoder decoder; std::size_t i = 0; + std::size_t range = 0; if (progress) progress(0, limit); std::size_t lastReport = 0; while (i < limit) { + while (range < unwindRanges.size() && unwindRanges[range].End <= i) ++range; + const bool outsideUnwindRanges = !unwindRanges.empty() && (range == unwindRanges.size() || i < unwindRanges[range].Begin); + const std::size_t nextUnwindRange = outsideUnwindRanges && range < unwindRanges.size() ? unwindRanges[range].Begin : limit; const std::uint8_t* cursor = codeSection.data() + i; const std::size_t available = limit - i; @@ -38,7 +60,14 @@ void ScanCodeSection(const std::vector& codeSection, const Domain: length = decoder.Decode(cursor, available); } catch (const CodegenException& e) { if (std::all_of(cursor, cursor + available, [](const std::uint8_t b) { return b == 0; })) break; - throw CodegenException(std::string("Cannot decode instruction: ") + e.what(), codeSectionOffset + i); + if (!outsideUnwindRanges) throw CodegenException(std::string("Cannot decode instruction: ") + e.what(), codeSectionOffset + i); + ++i; + continue; + } + + if (length > nextUnwindRange - i) { + i = nextUnwindRange; + continue; } const InstructionMatch match{codeSectionOffset + i, length}; @@ -56,16 +85,16 @@ void ScanCodeSection(const std::vector& codeSection, const Domain: std::vector InstructionScanner::ScanCodeSection(const std::vector& codeSection, const Domain::FileByteOffset codeSectionOffset, const Domain::FileByteOffset codeSectionSize) const { std::vector matches; - Codegen::ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize, progress, [&](const InstructionMatch& match) { matches.push_back(match); }); + Codegen::ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize, progress, unwindRanges, [&](const InstructionMatch& match) { matches.push_back(match); }); return matches; } void InstructionScanner::VisitCodeSection(const std::vector& codeSection, const Domain::FileByteOffset codeSectionOffset, const Domain::FileByteOffset codeSectionSize, const std::function& visit) const { - Codegen::ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize, progress, visit); + Codegen::ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize, progress, unwindRanges, visit); } -std::unique_ptr MakeInstructionScanner(std::function progress) { - return std::make_unique(std::move(progress)); +std::unique_ptr MakeInstructionScanner(std::function progress, std::vector unwindRanges) { + return std::make_unique(std::move(progress), std::move(unwindRanges)); } } diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index c9128ecbaf..0825fa3986 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -108,6 +108,14 @@ Amd64OnlyMatch _inPlace(const Entry& entry, const std::size_t length, std::vecto return Amd64OnlyMatch{entry.Name, length, Amd64OnlyLowering::InPlace, std::move(replacement), {}, 0}; } +void _emitRdpru(StubBodyBuilder& body) { + body.Raw(std::span(kLeaRspBelowRedZone.Bytes, kLeaRspBelowRedZone.Size)); + body.Raw(std::span(kRdpruTrampolineBytes, sizeof(kRdpruTrampolineBytes))); + body.Raw(std::span(kLeaRspRestore.Bytes, kLeaRspRestore.Size)); +} + +constexpr std::uint8_t kMcommitStc = 0xF9; + void _emitScalarCompare(StubBodyBuilder& body, const DecodedInstruction& instr, const ScalarCompareOperands& operands) { const std::span bytes(instr.Data, instr.Length); if (operands.RipDisplacementOffset) @@ -180,6 +188,8 @@ private: [[nodiscard]] Amd64OnlyMatch _matchSha256(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] Amd64OnlyMatch _matchSha1(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] Amd64OnlyMatch _matchClzero(const DecodedInstruction& instr, std::span trailing) const; + [[nodiscard]] Amd64OnlyMatch _matchRdpru(const DecodedInstruction& instr, std::span trailing) const; + [[nodiscard]] Amd64OnlyMatch _matchMcommit(const DecodedInstruction& instr) const; [[nodiscard]] std::optional _matchRosetta(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] const char* _emitOutOfLine(StubBodyBuilder& body, const DecodedInstruction& instr) const; }; @@ -222,6 +232,18 @@ Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchClzero(const DecodedInstructio return _trampoline(kClzero.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _clzeroLowering.EmitOutOfLine(body, operands); })); } +Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchRdpru(const DecodedInstruction& instr, std::span trailing) const { + if (!_validWait(instr)) + return _unsupported(kRdpru, instr.Length); + return _trampoline(kRdpru.Name, instr.Length, _outOfLine(instr.Length, trailing, [](StubBodyBuilder& body) { _emitRdpru(body); })); +} + +Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchMcommit(const DecodedInstruction& instr) const { + if (!_validWait(instr)) + return _unsupported(kMcommit, instr.Length); + return _inPlace(kMcommit, instr.Length, {kMcommitStc}); +} + std::optional Amd64OnlyInstructionMatcher::_matchRosetta(const DecodedInstruction& instr, std::span trailing) const { if (_target != Amd64OnlyTarget::Rosetta) return std::nullopt; @@ -269,6 +291,18 @@ const char* Amd64OnlyInstructionMatcher::_emitOutOfLine(StubBodyBuilder& body, c _reciprocalLowering.EmitOutOfLine(body, *reciprocal); return _reciprocalEntry(*reciprocal).Name; } + if (instr.IsRdpru()) { + if (!_validWait(instr)) + return nullptr; + _emitRdpru(body); + return kRdpru.Name; + } + if (instr.IsMcommit()) { + if (!_validWait(instr)) + return nullptr; + body.Raw(std::span(&kMcommitStc, 1)); + return kMcommit.Name; + } if (_target == Amd64OnlyTarget::Rosetta && instr.IsRdpid()) { body.Raw(_rdpidReplacement(instr)); return kRdpid.Name; @@ -363,10 +397,10 @@ std::optional Amd64OnlyInstructionMatcher::Match( return _unsupported(kClzero, length); if (instr.IsRdpru()) - return _unsupported(kRdpru, length); + return _matchRdpru(instr, trailing); if (instr.IsMcommit()) - return _unsupported(kMcommit, length); + return _matchMcommit(instr); return _matchRosetta(instr, trailing); } diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 1072a35a1a..3e77c91db6 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -350,7 +350,16 @@ void matcherSubstitutions() { require(prefixedClzero && prefixedClzero->Lowering == Codegen::Amd64OnlyLowering::Unsupported, "CLZERO with a 66, F2 or F3 prefix was not reported as unsupported"); } const auto rdpru = match({0x0F, 0x01, 0xFD}); - require(rdpru && rdpru->Lowering == Codegen::Amd64OnlyLowering::Unsupported && rdpru->InstructionName == "RDPRU", "RDPRU was not reported as unsupported"); + require(rdpru && rdpru->Lowering == Codegen::Amd64OnlyLowering::Trampoline && rdpru->InstructionName == "RDPRU" && rdpru->StubBody.size() == 40 && rdpru->ReturnBranchOffset == 35 && rdpru->Relocations.empty(), "RDPRU was not lowered through a runtime-checked stub"); + require(Bytes(rdpru->StubBody.begin(), rdpru->StubBody.begin() + 35) == Bytes{0x48, 0x8D, 0xA4, 0x24, 0x70, 0xFF, 0xFF, 0xFF, 0x9C, 0x85, 0xC9, 0x74, 0x0B, 0x83, 0xF9, 0x01, 0x74, 0x06, 0x31, 0xC0, 0x31, 0xD2, 0xEB, 0x02, 0x0F, 0x31, 0x9D, 0x48, 0x8D, 0xA4, 0x24, 0x90, 0x00, 0x00, 0x00}, "RDPRU stub does not frame the red zone or dispatch ECX to TSC or zero"); + const auto mcommit = match({0xF3, 0x0F, 0x01, 0xFA}); + require(mcommit && mcommit->Lowering == Codegen::Amd64OnlyLowering::InPlace && mcommit->InstructionName == "MCOMMIT" && mcommit->ReplacementBytes == Bytes{0xF9, 0x0F, 0x1F, 0x00}, "MCOMMIT was not replaced by STC plus NOP"); + const auto prefixedMcommit = match({0x2E, 0xF3, 0x0F, 0x01, 0xFA}); + require(prefixedMcommit && prefixedMcommit->Lowering == Codegen::Amd64OnlyLowering::InPlace && prefixedMcommit->ReplacementBytes == Bytes{0xF9, 0x0F, 0x1F, 0x40, 0x00}, "Prefixed MCOMMIT was not padded to its length"); + const auto lockedMcommit = match({0xF0, 0xF3, 0x0F, 0x01, 0xFA}); + require(lockedMcommit && lockedMcommit->Lowering == Codegen::Amd64OnlyLowering::Unsupported, "LOCK MCOMMIT was replaced instead of failing"); + const auto lockedRdpru = match({0xF0, 0x0F, 0x01, 0xFD}); + require(lockedRdpru && lockedRdpru->Lowering == Codegen::Amd64OnlyLowering::Unsupported, "LOCK RDPRU was replaced instead of failing"); const auto registerForm = match({0x66, 0x0F, 0x79, 0xCA}); require(registerForm && registerForm->Lowering == Codegen::Amd64OnlyLowering::Trampoline && registerForm->InstructionName == "EXTRQ register form", "EXTRQ register form was not lowered through a stub"); const auto insertqRegisterForm = match({0xF2, 0x0F, 0x79, 0xCA}); @@ -490,12 +499,12 @@ void converterSegment() { auto branchInside = file; branchInside[0x207] = 0x02; requireFailure([&] { (void)converter->Convert(branchInside, {segmentHeader(20)}); }, "Branch into an AMD-only instruction was accepted"); - auto rdpru = file; - rdpru[0x20F] = 0x0F; - rdpru[0x210] = 0x01; - rdpru[0x211] = 0xFD; - rdpru[0x212] = 0x90; - requireFailure([&] { (void)converter->Convert(rdpru, {segmentHeader(20)}); }, "RDPRU was silently kept"); + auto lockedRdpru = file; + lockedRdpru[0x20F] = 0xF0; + lockedRdpru[0x210] = 0x0F; + lockedRdpru[0x211] = 0x01; + lockedRdpru[0x212] = 0xFD; + requireFailure([&] { (void)converter->Convert(lockedRdpru, {segmentHeader(20)}); }, "LOCK RDPRU was accepted"); auto registerForm = file; const Bytes extrqRegister = {0x66, 0x0F, 0x79, 0xCA}; std::copy(extrqRegister.begin(), extrqRegister.end(), registerForm.begin() + 0x20F); @@ -612,15 +621,16 @@ void converterSha1() { void converterMonitorWait() { const auto converter = Codegen::MakeAmd64OnlyConverter(); Bytes file(0x300, 0xCC); - const Bytes text = {0x0F, 0x01, 0xFA, 0x0F, 0x01, 0xFB, 0x2E, 0x0F, 0x01, 0xFB, 0xC3}; + const Bytes text = {0x0F, 0x01, 0xFA, 0x0F, 0x01, 0xFB, 0x2E, 0x0F, 0x01, 0xFB, 0xF3, 0x0F, 0x01, 0xFA, 0xC3}; std::copy(text.begin(), text.end(), file.begin() + 0x200); const auto result = converter->Convert(file, {segmentHeader(text.size())}); - require(result.ReplacedCount == 3 && result.Trampolines.empty() && result.Reports.size() == 3, "MONITORX/MWAITX were not replaced in place"); + require(result.ReplacedCount == 4 && result.Trampolines.empty() && result.Reports.size() == 4, "MONITORX/MWAITX/MCOMMIT were not replaced in place"); require(result.Reports[0].InstructionName == "MONITORX" && result.Reports[1].InstructionName == "MWAITX" && result.Reports[2].Offset == 0x206 && result.Reports[2].ReplacementLength == 4 && result.Reports[2].Lowering == Codegen::Amd64OnlyLowering::InPlace, "MONITORX/MWAITX reports are wrong"); + require(result.Reports[3].InstructionName == "MCOMMIT" && result.Reports[3].Offset == 0x20A && result.Reports[3].ReplacementLength == 4 && result.Reports[3].Lowering == Codegen::Amd64OnlyLowering::InPlace, "MCOMMIT report is wrong"); auto expected = file; - const Bytes replaced = {0x0F, 0x1F, 0x00, 0xF3, 0x90, 0x90, 0xF3, 0x90, 0x66, 0x90, 0xC3}; + const Bytes replaced = {0x0F, 0x1F, 0x00, 0xF3, 0x90, 0x90, 0xF3, 0x90, 0x66, 0x90, 0xF9, 0x0F, 0x1F, 0x00, 0xC3}; std::copy(replaced.begin(), replaced.end(), expected.begin() + 0x200); - require(result.Bytes == expected, "MONITORX/MWAITX were replaced with the wrong bytes"); + require(result.Bytes == expected, "MONITORX/MWAITX/MCOMMIT were replaced with the wrong bytes"); } void converterClzero() { @@ -1215,10 +1225,10 @@ void converterFailureOffsets() { auto movntsRegister = file; movntsRegister[0x212] = 0xC1; require(failureOffset([&] { (void)converter->Convert(movntsRegister, {segmentHeader(20)}); }, "MOVNTSS register form was accepted") == 0x20F, "MOVNTSS failure does not carry the file offset"); - auto rdpru = file; - const Bytes rdpruBytes = {0x0F, 0x01, 0xFD, 0x90}; - std::copy(rdpruBytes.begin(), rdpruBytes.end(), rdpru.begin() + 0x20F); - require(failureOffset([&] { (void)converter->Convert(rdpru, {segmentHeader(20)}); }, "RDPRU was accepted") == 0x20F, "Unsupported instruction failure does not carry the file offset"); + auto lockedWait = file; + const Bytes lockedWaitBytes = {0xF0, 0x0F, 0x01, 0xFB}; + std::copy(lockedWaitBytes.begin(), lockedWaitBytes.end(), lockedWait.begin() + 0x20F); + require(failureOffset([&] { (void)converter->Convert(lockedWait, {segmentHeader(20)}); }, "LOCK MWAITX was accepted") == 0x20F, "Unsupported instruction failure does not carry the file offset"); } Bytes elfFixture(const Bytes& text) { @@ -1807,6 +1817,92 @@ void ripRelativeExecution() { require(movedRun.Rax == movedRun.SiteAddress + 4096 + 0x30, "Moved RIP-relative LEA did not compute the original address"); unchanged(movedRun, (1u << 1) | (1u << 3), "Stub with moved RIP-relative instructions clobbered a register or RFLAGS"); } + +void rdpruExecution() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(); + const Bytes site = {0x0F, 0x01, 0xFD}; + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "RDPRU stub was not produced"); + auto body = match->StubBody; + const auto ret = body.size(); + body.push_back(0xC3); + const auto displacement = static_cast(ret - (match->ReturnBranchOffset + 5)); + std::memcpy(body.data() + match->ReturnBranchOffset + 1, &displacement, sizeof(displacement)); + 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 RDPRU stub"); + std::memcpy(code, body.data(), body.size()); + for (const std::uint32_t selector : {0u, 1u, 2u, 0xFFFFFFFFu}) { + std::uint64_t raxOut = 0; + std::uint64_t rdxOut = 0; + std::uint64_t rcxOut = 0; + std::uint64_t flagsOut = 0; + std::uint64_t canaryOut = 0; + asm volatile( + "sub $128, %%rsp\n\t" + "movabs $0xA5A5A5A5A5A5A5A5, %%rax\n\t" + "lea -136(%%rsp), %%rdi\n\t" + "mov $16, %%ecx\n\t" + "rep stosq\n\t" + "movl %[selector], %%ecx\n\t" + "pushq $0x8D7\n\t" + "popfq\n\t" + "call *%[code]\n\t" + "mov %%rax, %[rax]\n\t" + "mov %%rdx, %[rdx]\n\t" + "mov %%rcx, %[rcx]\n\t" + "pushfq\n\t" + "popq %[flags]\n\t" + "xor %%r8, %%r8\n\t" + "lea -136(%%rsp), %%rsi\n\t" + "mov $16, %%ecx\n\t" + "1:\n\t" + "xor (%%rsi), %%r8\n\t" + "add $8, %%rsi\n\t" + "dec %%ecx\n\t" + "jnz 1b\n\t" + "mov %%r8, %[canary]\n\t" + "add $128, %%rsp\n\t" + : [rax] "=r"(raxOut), [rdx] "=r"(rdxOut), [rcx] "=r"(rcxOut), [flags] "=r"(flagsOut), [canary] "=r"(canaryOut) + : [selector] "r"(selector), [code] "r"(code) + : "rax", "rcx", "rdx", "rsi", "rdi", "r8", "memory", "cc"); + require(canaryOut == 0, "RDPRU stub clobbered the red zone"); + require(rcxOut == selector, "RDPRU stub changed ecx"); + require((flagsOut & 0x8D5) == (0x8D7 & 0x8D5), "RDPRU stub changed RFLAGS"); + if (selector <= 1) + require((raxOut | rdxOut) != 0, "RDPRU MPERF/APERF selector did not yield the TSC approximation"); + else + require(raxOut == 0 && rdxOut == 0, "RDPRU with an undefined selector did not yield zero"); + } + munmap(code, 4096); +} + +void mcommitExecution() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(); + const Bytes site = {0xF3, 0x0F, 0x01, 0xFA}; + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::InPlace, "MCOMMIT lowering was not produced"); + 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 MCOMMIT lowering"); + std::memcpy(code, body.data(), body.size()); + for (const std::uint64_t flagsIn : {std::uint64_t{0x8D6}, std::uint64_t{0x8D7}}) { + std::uint64_t flagsOut = 0; + asm volatile( + "mov %[in], %%rax\n\t" + "pushq %%rax\n\t" + "popfq\n\t" + "call *%[code]\n\t" + "pushfq\n\t" + "popq %[out]\n\t" + : [out] "=r"(flagsOut) + : [in] "r"(flagsIn), [code] "r"(code) + : "rax", "memory", "cc"); + require((flagsOut & 1) == 1, "MCOMMIT lowering did not report committed"); + require((flagsOut & 0x8D4) == (flagsIn & 0x8D4), "MCOMMIT lowering changed flags other than CF"); + } + munmap(code, 4096); +} #else void reciprocalExecution() {} void registerFormExecution() {} @@ -1815,6 +1911,8 @@ void sha256Execution() {} void clzeroExecution() {} void immediateFormExecution() {} void ripRelativeExecution() {} +void rdpruExecution() {} +void mcommitExecution() {} #endif #if defined(__x86_64__) && (defined(__linux__) || defined(__APPLE__)) @@ -1859,6 +1957,18 @@ void scannerZeroTail() { requireFailure([&] { (void)scanner->ScanCodeSection(truncated, 0, truncated.size()); }, "Truncated non-zero tail must still fail"); } +void scannerUnwindRanges() { + const Bytes slot{0x48, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x2E, 0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC3}; + const auto gap = Codegen::MakeInstructionScanner({}, {{16, 17}})->ScanCodeSection(slot, 0x1000, slot.size()); + require(gap.size() == 2 && gap[0].Offset == 0x1001 && gap[0].Length == 15 && gap[1].Offset == 0x1010 && gap[1].Length == 1, "An undecodable byte outside every unwind range must resume the scan at the next byte"); + requireFailure([&] { (void)Codegen::MakeInstructionScanner({}, {{0, 17}})->ScanCodeSection(slot, 0, slot.size()); }, "An undecodable byte inside an unwind range must still fail"); + requireFailure([&] { (void)Codegen::MakeInstructionScanner()->ScanCodeSection(slot, 0, slot.size()); }, "An undecodable byte without unwind ranges must still fail"); + const Bytes crossing{0xB8, 0x0F, 0x05, 0xC3, 0x90}; + const auto cut = Codegen::MakeInstructionScanner({}, {{3, 4}, {1, 3}})->ScanCodeSection(crossing, 0, crossing.size()); + require(cut.size() == 3 && cut[0].Offset == 1 && cut[0].Length == 2 && cut[1].Offset == 3 && cut[2].Offset == 4, "A gap instruction that crosses into an unwind range must end at the range"); + require(Codegen::MakeInstructionScanner()->ScanCodeSection(crossing, 0, crossing.size()).front().Length == 5, "Without unwind ranges the crossing instruction must decode whole"); +} + int main() { try { decoderLengths(); @@ -1880,6 +1990,8 @@ int main() { clzeroExecution(); reciprocalExecution(); ripRelativeExecution(); + rdpruExecution(); + mcommitExecution(); converterSegment(); converterRipRelativeFollower(); converterSha256(); @@ -1905,6 +2017,7 @@ int main() { linuxRipRelativePlacement(); linuxStackSynthesis(); scannerZeroTail(); + scannerUnwindRanges(); std::cout << "AMD64-only converter tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/relinker/domain/include/domain/Types.hpp b/core/relinker/domain/include/domain/Types.hpp index f505333ec7..dd181853e0 100644 --- a/core/relinker/domain/include/domain/Types.hpp +++ b/core/relinker/domain/include/domain/Types.hpp @@ -16,6 +16,12 @@ using FileByteOffset = std::uint64_t; using VirtualAddress = std::uint64_t; using ByteCount = std::uint64_t; +struct CodeRange { + ByteCount Begin; + ByteCount End; + bool operator==(const CodeRange&) const = default; +}; + struct ElfHeader { std::uint16_t Machine; std::uint16_t Type; diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index 91a7665452..3ba5513d8c 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp @@ -383,7 +383,7 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { const auto& headers = *input.Headers; const auto imageBase = image.GetImageBase(); const auto tls = ScanTls(*input.Source, headers, image); - const bool hasTls = tls.Tls != nullptr && tls.Tls->MemorySize != 0; + const bool hasTls = tls.Tls != nullptr && (tls.Tls->MemorySize != 0 || !tls.Accesses.empty()); if (!hasTls && !tls.Accesses.empty()) throw Domain::RelinkerException("Guest TLS access without a non-empty PT_TLS"); if (!hasTls && !input.TlsModuleSlots.empty()) throw Domain::RelinkerException("Guest TLS module relocation without a non-empty PT_TLS"); diff --git a/core/relinker/elfpatcher/tests/LinuxDesktopEntryTests.cpp b/core/relinker/elfpatcher/tests/LinuxDesktopEntryTests.cpp index bfbaf19eb6..0c473271cd 100644 --- a/core/relinker/elfpatcher/tests/LinuxDesktopEntryTests.cpp +++ b/core/relinker/elfpatcher/tests/LinuxDesktopEntryTests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -131,7 +132,8 @@ void run(const std::filesystem::path& root) { } int main() { - const auto root = std::filesystem::temp_directory_path() / "anyps5-desktop-entry-tests"; + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-desktop-entry-tests-" + std::to_string(std::random_device{}())); std::filesystem::remove_all(root); try { run(root); diff --git a/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp b/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp index eb7a8d7e36..fa6c1d4085 100644 --- a/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp +++ b/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -57,6 +58,33 @@ void checkRuntimeDependencies() { check({"libSceVideoOut.prx", "libSceVideoOut.prx"}, {"libSceVideoOut.prx"}); } +template +void openFixture(TStream& stream, const Fs::path& path, std::ios::openmode mode) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + for (;;) { + errno = 0; + _set_doserrno(0); + stream.clear(); + stream.open(path, mode); + if (stream.is_open() || _doserrno != ERROR_SHARING_VIOLATION || std::chrono::steady_clock::now() >= deadline) + return; + Sleep(10); + } +} + +void removeFixtures(const Fs::path& directory) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + for (;;) { + std::error_code error; + Fs::remove_all(directory, error); + if (!error) + return; + if (error != std::errc::permission_denied || std::chrono::steady_clock::now() >= deadline) + throw Fs::filesystem_error("cannot remove diagnostic fixtures", directory, error); + Sleep(10); + } +} + void writeFile(const Fs::path& path, const std::vector& bytes) { const auto fail = [&](const char* phase) { const auto error = errno; @@ -66,9 +94,8 @@ void writeFile(const Fs::path& path, const std::vector& bytes) { " native_error=" + std::to_string(nativeError) + " bytes=" + std::to_string(bytes.size())); }; - errno = 0; - _set_doserrno(0); - std::ofstream stream(path, std::ios::binary); + std::ofstream stream; + openFixture(stream, path, std::ios::binary); if (!stream.is_open()) fail("open"); if (!stream.write(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) @@ -251,7 +278,8 @@ int main() { createImage(root, {{"KERNEL32.dll", "RelinkerDiagnosticsMissingExport"}}, {}); expectDiagnostic(runner, {"Symbol: RelinkerDiagnosticsMissingExport"}); { - std::fstream corrupt(root, std::ios::binary | std::ios::in | std::ios::out); + std::fstream corrupt; + openFixture(corrupt, root, std::ios::binary | std::ios::in | std::ios::out); const std::array invalidRva{static_cast(0xf0), static_cast(0xff), static_cast(0xff), static_cast(0xff)}; corrupt.seekp(LoadRva + 0x300); if (!corrupt.write(invalidRva.data(), invalidRva.size())) @@ -272,7 +300,7 @@ int main() { createImage(root, {{middle.filename().string(), "Middle"}}, {}); writeFile(middle, elf); expectDiagnostic(runner, {"dependency module is not a Windows PE image: " + middle.string(), "Importer: " + root.string(), "Chain: " + runner.string() + " -> " + root.string() + " -> " + middle.string() + "\n"}); - Fs::remove_all(directory); + removeFixtures(directory); std::cout << "Windows dependency machine-code tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp b/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp index c43fd18cd6..4e897a7a0b 100644 --- a/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp +++ b/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp @@ -8,6 +8,7 @@ namespace Relinker { std::optional FindSceCodeEnd(std::span segment, VirtualAddress segmentVAddr, const std::vector& functions, VirtualAddress pltGot, ByteCount pltRelocationsSize); +std::vector SceUnwindRanges(std::span segment, VirtualAddress segmentVAddr, const std::vector& functions); } diff --git a/core/relinker/relinker/include/relinker/domain/ICallSiteResolver.hpp b/core/relinker/relinker/include/relinker/domain/ICallSiteResolver.hpp index 93e53570cd..8a3e5df7dc 100644 --- a/core/relinker/relinker/include/relinker/domain/ICallSiteResolver.hpp +++ b/core/relinker/relinker/include/relinker/domain/ICallSiteResolver.hpp @@ -14,7 +14,8 @@ public: const std::vector& textSection, FileByteOffset textSectionVAddr, VirtualAddress targetGotOrPltAddress, - ByteCount targetGotOrPltSize + ByteCount targetGotOrPltSize, + const std::vector& unwindRanges = {} ) = 0; }; diff --git a/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp b/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp index 71a9160da0..a4adf4a77c 100644 --- a/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp +++ b/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp @@ -12,7 +12,7 @@ class ISyscallScanner { public: virtual ~ISyscallScanner() = default; - virtual std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input = {}) = 0; + virtual std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input = {}, const std::vector& unwindRanges = {}) = 0; }; } diff --git a/core/relinker/relinker/include/relinker/domain/Types.hpp b/core/relinker/relinker/include/relinker/domain/Types.hpp index 0cae3ed174..856d6a0154 100644 --- a/core/relinker/relinker/include/relinker/domain/Types.hpp +++ b/core/relinker/relinker/include/relinker/domain/Types.hpp @@ -8,6 +8,7 @@ namespace Relinker { using FileByteOffset = Domain::FileByteOffset; using VirtualAddress = Domain::VirtualAddress; using ByteCount = Domain::ByteCount; +using CodeRange = Domain::CodeRange; using ElfHeader = Domain::ElfHeader; using ProgramHeader = Domain::ProgramHeader; using SectionHeader = Domain::SectionHeader; diff --git a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp index 63a1b2450c..91fe531f9c 100644 --- a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp +++ b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp @@ -51,21 +51,22 @@ std::optional ripRelativeDispOffset(const std::vector class CallSiteResolver : public ICallSiteResolver { public: - std::vector ResolveCallSites(const std::vector& textSection, FileByteOffset textSectionVAddr, VirtualAddress targetGotOrPltAddress, ByteCount targetGotOrPltSize) override; + std::vector ResolveCallSites(const std::vector& textSection, FileByteOffset textSectionVAddr, VirtualAddress targetGotOrPltAddress, ByteCount targetGotOrPltSize, const std::vector& unwindRanges) override; private: const std::vector* _cachedTextPtr = nullptr; std::vector> _cachedTargetToInstr; FileByteOffset _cachedTextVAddr = 0; + std::vector _cachedUnwindRanges; }; -std::vector CallSiteResolver::ResolveCallSites(const std::vector& textSection, const FileByteOffset textSectionVAddr, const VirtualAddress targetGotOrPltAddress, const ByteCount targetGotOrPltSize) { +std::vector CallSiteResolver::ResolveCallSites(const std::vector& textSection, const FileByteOffset textSectionVAddr, const VirtualAddress targetGotOrPltAddress, const ByteCount targetGotOrPltSize, const std::vector& unwindRanges) { if (targetGotOrPltSize > std::numeric_limits::max() - targetGotOrPltAddress) throw RelinkerException("Relocation target range exceeds the address space"); if (textSection.size() > std::numeric_limits::max() - textSectionVAddr) throw RelinkerException("Text section range exceeds the address space"); if (textSection.empty()) return {}; - if (_cachedTextPtr != &textSection || _cachedTextVAddr != textSectionVAddr) { - auto scanner = Codegen::MakeInstructionScanner(); + if (_cachedTextPtr != &textSection || _cachedTextVAddr != textSectionVAddr || _cachedUnwindRanges != unwindRanges) { + auto scanner = Codegen::MakeInstructionScanner({}, unwindRanges); _cachedTargetToInstr.clear(); const auto candidateCount = std::count_if(textSection.begin(), textSection.end(), [](std::uint8_t b) { return b == OneByteGrp5Rm || b == OneByteModRmRangeJMax; @@ -83,6 +84,7 @@ std::vector CallSiteResolver::ResolveCallSites(const std::vector std::sort(_cachedTargetToInstr.begin(), _cachedTargetToInstr.end()); _cachedTextPtr = &textSection; _cachedTextVAddr = textSectionVAddr; + _cachedUnwindRanges = unwindRanges; } std::vector sites; diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index f432b0cdcd..e7a89396b1 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -198,13 +198,21 @@ std::set CodeInstructionCollector::Collect(const std::ve } CodePointers pointers; std::set importSlots; - const auto readRelocations = [&](std::uint64_t addressTag, std::uint64_t sceAddressTag, std::uint64_t sizeTag, std::uint64_t sceSizeTag) { + const auto relocationRange = [&](std::uint64_t addressTag, std::uint64_t sceAddressTag, std::uint64_t sizeTag, std::uint64_t sceSizeTag) -> std::pair { const auto size = value(sizeTag, sceSizeTag); - if (!size) return; + if (!size) return {0, 0}; if (*size % 24 != 0) throw Domain::RelinkerException("Code analysis: invalid relocation table size"); - if (*size == 0) return; - const auto start = table(addressTag, sceAddressTag, *size); - for (std::uint64_t offset = start; offset < start + *size; offset += 24) { + if (*size == 0) return {0, 0}; + return {table(addressTag, sceAddressTag, *size), *size}; + }; + const auto [relaOffset, relaSize] = relocationRange(7, 0x6100002f, 8, 0x61000031); + const auto [pltOffset, pltSize] = relocationRange(23, 0x61000029, 2, 0x6100002d); + if (relaSize != 0 && pltSize != 0 && relaOffset < pltOffset + pltSize && pltOffset < relaOffset + relaSize + && relaOffset % 24 != pltOffset % 24) + throw Domain::RelinkerException("Code analysis: unaligned overlapping relocation tables", pltOffset); + const auto readRelocations = [&](std::uint64_t start, std::uint64_t size, bool plt) { + for (std::uint64_t offset = start; offset < start + size; offset += 24) { + if (!plt && offset >= pltOffset && offset - pltOffset < pltSize) continue; const auto slot = Io::ReadU64(bytes, offset); const auto info = Io::ReadU64(bytes, offset + 8); const auto type = static_cast(info); @@ -231,8 +239,8 @@ std::set CodeInstructionCollector::Collect(const std::ve } } }; - readRelocations(7, 0x6100002f, 8, 0x61000031); - readRelocations(23, 0x61000029, 2, 0x6100002d); + readRelocations(relaOffset, relaSize, false); + readRelocations(pltOffset, pltSize, true); for (const auto& [addressTag, sizeTag] : {std::pair{25u, 27u}, std::pair{26u, 28u}, std::pair{32u, 33u}}) { const auto address = value(addressTag, 0x60000000 + addressTag); const auto size = value(sizeTag, 0x60000000 + sizeTag); diff --git a/core/relinker/relinker/src/analysis/SceCodeExtent.cpp b/core/relinker/relinker/src/analysis/SceCodeExtent.cpp index d1c84c915e..3de6710b89 100644 --- a/core/relinker/relinker/src/analysis/SceCodeExtent.cpp +++ b/core/relinker/relinker/src/analysis/SceCodeExtent.cpp @@ -47,4 +47,12 @@ std::optional FindSceCodeEnd(std::span segme throw RelinkerException("PLT not found before or after the unwind functions", codeEnd); } +std::vector SceUnwindRanges(std::span segment, VirtualAddress segmentVAddr, const std::vector& functions) { + std::vector ranges; + for (const auto& function : functions) + if (function.Begin >= segmentVAddr && function.Begin - segmentVAddr < segment.size() && function.End > function.Begin) + ranges.push_back({function.Begin - segmentVAddr, std::min(function.End - segmentVAddr, segment.size())}); + return ranges; +} + } diff --git a/core/relinker/relinker/src/analysis/SyscallScanner.cpp b/core/relinker/relinker/src/analysis/SyscallScanner.cpp index 6dc9546265..08d5e87dcb 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -174,20 +174,20 @@ class SyscallScanner : public ISyscallScanner { public: SyscallScanner(Domain::IProgress& progress, const bool trapSdkStubs) : progress(progress), trapSdkStubs(trapSdkStubs) {} - std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input) override; + std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input, const std::vector& unwindRanges) override; private: Domain::IProgress& progress; bool trapSdkStubs; }; -std::vector SyscallScanner::ScanCodeSectionForSyscalls(const std::vector& codeSection, const FileByteOffset codeSectionOffset, const FileByteOffset codeSectionSize, std::string_view input) { +std::vector SyscallScanner::ScanCodeSectionForSyscalls(const std::vector& codeSection, const FileByteOffset codeSectionOffset, const FileByteOffset codeSectionSize, std::string_view input, const std::vector& unwindRanges) { if (codeSectionSize > codeSection.size()) throw RelinkerException("Code section exceeds the input buffer", codeSectionOffset); if (codeSectionSize > std::numeric_limits::max() - codeSectionOffset) throw RelinkerException("Code section offset overflow", codeSectionOffset); const auto limit = static_cast(codeSectionSize); const std::string source(input); - const auto scanner = Codegen::MakeInstructionScanner([&](auto completed, auto total) { progress.Report({Domain::ProgressStage::Decode, source, completed, total}); }); + const auto scanner = Codegen::MakeInstructionScanner([&](auto completed, auto total) { progress.Report({Domain::ProgressStage::Decode, source, completed, total}); }, unwindRanges); RecentInstructions recent; std::vector trapped; scanner->VisitCodeSection(codeSection, codeSectionOffset, codeSectionSize, [&](const Codegen::InstructionMatch& match) { @@ -232,7 +232,7 @@ std::unique_ptr MakeSyscallScanner(Domain::IProgress& progress, class NullSyscallScanner : public ISyscallScanner { public: - std::vector ScanCodeSectionForSyscalls(const std::vector&, FileByteOffset, FileByteOffset, std::string_view) override { return {}; } + std::vector ScanCodeSectionForSyscalls(const std::vector&, FileByteOffset, FileByteOffset, std::string_view, const std::vector&) override { return {}; } }; std::unique_ptr MakeNullSyscallScanner() { diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index cf296e8b95..3e9a239afa 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace Relinker { @@ -213,15 +214,26 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (name.empty() || name.find_first_of("\\:$\r\n") != std::string::npos) fail("Invalid dependency: " + name); if (dependencies.insert(name).second) image.Dependencies.push_back(name); } - std::map relocationTargets; - const auto copyRelocations = [&](std::uint64_t addressTag, std::uint64_t sceAddressTag, std::uint64_t sizeTag, std::uint64_t sceSizeTag, std::vector& output, bool plt) { - if (!tags.contains(addressTag) && !tags.contains(sceAddressTag) && !tags.contains(sizeTag) && !tags.contains(sceSizeTag)) return; + const auto relocationRange = [&](std::uint64_t addressTag, std::uint64_t sceAddressTag, std::uint64_t sizeTag, std::uint64_t sceSizeTag) -> std::pair { + if (!tags.contains(addressTag) && !tags.contains(sceAddressTag) && !tags.contains(sizeTag) && !tags.contains(sceSizeTag)) return {0, 0}; const auto size = get(sizeTag, sceSizeTag); if (size % 24 != 0) fail("Invalid relocation table size"); - const auto offset = table(addressTag, sceAddressTag, size); - if (plt && size != 0 && get(20, 0x6100002b) != 7) fail("Unsupported PLT relocation format"); + return {table(addressTag, sceAddressTag, size), size}; + }; + if (tags.contains(7) || tags.contains(0x6100002f)) { + if (get(9, 0x61000033) != 24) fail("Invalid RELA entry size"); + } + const auto [relaOffset, relaSize] = relocationRange(7, 0x6100002f, 8, 0x61000031); + const auto [pltOffset, pltSize] = relocationRange(23, 0x61000029, 2, 0x6100002d); + if (pltSize != 0 && get(20, 0x6100002b) != 7) fail("Unsupported PLT relocation format"); + if (relaSize != 0 && pltSize != 0 && relaOffset < pltOffset + pltSize && pltOffset < relaOffset + relaSize + && relaOffset % 24 != pltOffset % 24) + fail("Unaligned overlapping relocation tables"); + std::map relocationTargets; + const auto copyRelocations = [&](std::uint64_t offset, std::uint64_t size, std::vector& output, bool plt) { output.reserve(size); for (std::uint64_t index = 0; index < size; index += 24) { + if (!plt && offset + index >= pltOffset && offset + index - pltOffset < pltSize) continue; const auto info = Io::ReadU64(bytes, offset + index + 8); const auto relocation = static_cast(info); if (relocation == 0) continue; @@ -256,11 +268,8 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector output.insert(output.end(), bytes.begin() + offset + index, bytes.begin() + offset + index + 24); } }; - if (tags.contains(7) || tags.contains(0x6100002f)) { - if (get(9, 0x61000033) != 24) fail("Invalid RELA entry size"); - } - copyRelocations(7, 0x6100002f, 8, 0x61000031, image.Dynamic.RelaData, false); - copyRelocations(23, 0x61000029, 2, 0x6100002d, image.Dynamic.RelaPltData, true); + copyRelocations(relaOffset, relaSize, image.Dynamic.RelaData, false); + copyRelocations(pltOffset, pltSize, image.Dynamic.RelaPltData, true); if (tags.contains(3) || tags.contains(0x61000027)) image.Got = get(3, 0x61000027); if (tags.contains(12)) image.Init = tags.at(12); if (tags.contains(13)) image.Fini = tags.at(13); diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 88a1450c60..cf903ef7a2 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -223,6 +223,13 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const auto found = findGuest(name); if (found != guestNames.end() && found->second != index) dependencies[index].insert(found->second); } + std::set referencedSymbols; + for (const auto* table : {&images[index].Dynamic.RelaData, &images[index].Dynamic.RelaPltData}) { + for (std::size_t offset = 0; offset < table->size(); offset += 24) { + const auto info = Io::ReadU64(*table, offset + 8); + if ((info & 0xffffffffu) != 0) referencedSymbols.insert(info >> 32); + } + } for (std::size_t symbolIndex = 0; symbolIndex < images[index].Symbols.size(); ++symbolIndex) { const auto& symbol = images[index].Symbols[symbolIndex]; if (symbol.Section != 0 || symbol.Name.empty()) continue; @@ -245,7 +252,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } if (provider != nullptr) { if ((symbol.Info & 15) != 0 && (symbol.Info & 15) != provider->Type) throw Domain::RelinkerException("Guest import/export type mismatch: " + symbol.Name); - if (provider->Provider != index) dependencies[index].insert(provider->Provider); + if (provider->Provider != index && referencedSymbols.contains(symbolIndex)) dependencies[index].insert(provider->Provider); } else if (windows && (symbol.Info & 15) == 6) throw Domain::RelinkerException("Windows guest TLS import requires a guest TLS export: " + symbol.Name); if (provider == nullptr) systemImports[index].insert(symbolIndex); } @@ -353,7 +360,8 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (sceImage) if (const auto end = FindSceCodeEnd(code, header.MappedAddress, unwindFunctions, image.Got, image.Dynamic.RelaPltData.size())) code.resize(*end - header.MappedAddress); - for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, code.size(), Io::Utf8Path(image.SourcePath))) { + const auto unwindRanges = SceUnwindRanges(code, header.MappedAddress, unwindFunctions); + for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, code.size(), Io::Utf8Path(image.SourcePath), unwindRanges)) { if (offset > code.size() || code.size() - offset < 2u) throw Domain::RelinkerException("Syscall trap patch exceeds guest executable segment", header.MappedAddress + offset); if (!image.PatchedRanges.empty()) { diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 022bfcc6f3..78ce7cd35a 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -257,6 +257,11 @@ RelinkResult RelinkerPipeline::Relink() { if (dynRelaOffset > raw.size() || dynRelaSize > raw.size() - dynRelaOffset) throw RelinkerException("Relocation table is out of bounds", dynRelaOffset); + if (dynRelaSize != 0 && dynJmpRelSize != 0 + && dynRelaOffset < dynJmpRelOffset + dynJmpRelSize && dynJmpRelOffset < dynRelaOffset + dynRelaSize + && dynRelaOffset % relaEntSize != dynJmpRelOffset % relaEntSize) + throw RelinkerException("Unaligned overlapping relocation tables", dynJmpRelOffset); + auto readCStr = [&](FileByteOffset strOff) -> std::string { if (strOff >= dynStrTabSize) throw RelinkerException("Dynamic string offset is outside DT_STRSZ", strOff); @@ -326,9 +331,11 @@ RelinkResult RelinkerPipeline::Relink() { return definedTlsSymbolValue(symIdx, pos).has_value(); }; - auto extractRela = [&](const FileByteOffset relaOff, const ByteCount relaSize) { + auto extractRela = [&](const FileByteOffset relaOff, const ByteCount relaSize, const bool plt) { for (ByteCount off = 0; relaSize >= relaEntSize && off <= relaSize - relaEntSize; off += relaEntSize) { const FileByteOffset pos = relaEntryPos(relaOff, off); + if (!plt && pos >= dynJmpRelOffset && pos - dynJmpRelOffset < dynJmpRelSize) + continue; std::uint64_t rOffset = 0, rInfo = 0; std::int64_t rAddend = 0; @@ -367,21 +374,24 @@ RelinkResult RelinkerPipeline::Relink() { } }; - extractRela(dynRelaOffset, dynRelaSize); - extractRela(dynJmpRelOffset, dynJmpRelSize); + extractRela(dynRelaOffset, dynRelaSize, false); + extractRela(dynJmpRelOffset, dynJmpRelSize, true); for (const auto& ref : nidRefs) validationPolicy->ValidateRelocationTypeSupported(ref.RelocationTypeValue, ref.RelocationTableOffset); 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 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 (auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { + 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); } } @@ -397,7 +407,7 @@ RelinkResult RelinkerPipeline::Relink() { for (std::size_t index = 0; index < executableSegments.size(); ++index) { auto& [segment, segmentVAddr, segmentFileOffset] = executableSegments[index]; auto& scanSegment = scanSegments[index]; - for (const auto offset : syscallScanner->ScanCodeSectionForSyscalls(scanSegment, segmentVAddr, scanSegment.size())) { + for (const auto offset : syscallScanner->ScanCodeSectionForSyscalls(scanSegment, segmentVAddr, scanSegment.size(), {}, segmentUnwindRanges[index])) { if (offset > segment.size() || segment.size() - offset < 2u) throw RelinkerException("Syscall trap patch exceeds executable segment", segmentVAddr + offset); segment[offset] = scanSegment[offset] = 0x0F; @@ -508,9 +518,11 @@ RelinkResult RelinkerPipeline::Relink() { std::memcpy(buf.data() + pos + 16, &addend, 8); }; - auto extractLocal = [&](const FileByteOffset relaOff, const ByteCount relaSize) { + auto extractLocal = [&](const FileByteOffset relaOff, const ByteCount relaSize, const bool plt) { for (ByteCount off = 0; relaSize >= relaEntSize && off <= relaSize - relaEntSize; off += relaEntSize) { const FileByteOffset pos = relaEntryPos(relaOff, off); + if (!plt && pos >= dynJmpRelOffset && pos - dynJmpRelOffset < dynJmpRelSize) + continue; std::uint64_t rOffset = 0, rInfo = 0; std::int64_t rAddend = 0; std::memcpy(&rOffset, raw.data() + pos, 8); @@ -540,8 +552,8 @@ RelinkResult RelinkerPipeline::Relink() { } }; - extractLocal(dynRelaOffset, dynRelaSize); - extractLocal(dynJmpRelOffset, dynJmpRelSize); + extractLocal(dynRelaOffset, dynRelaSize, false); + extractLocal(dynJmpRelOffset, dynJmpRelSize, true); std::vector entries; entries.reserve(nidRefs.size()); @@ -562,7 +574,7 @@ RelinkResult RelinkerPipeline::Relink() { for (std::size_t index = 0; index < executableSegments.size(); ++index) { const auto& segmentVAddr = std::get<1>(executableSegments[index]); for (auto& entry : entries) { - auto segmentSites = callSiteResolver->ResolveCallSites(scanSegments[index], segmentVAddr, entry.TargetOffset, 8); + auto segmentSites = callSiteResolver->ResolveCallSites(scanSegments[index], segmentVAddr, entry.TargetOffset, 8, segmentUnwindRanges[index]); entry.CallSites.insert(entry.CallSites.end(), segmentSites.begin(), segmentSites.end()); entry.CallSitesResolved = !entry.CallSites.empty(); } diff --git a/core/relinker/relinker/tests/guest_import_fixtures.py b/core/relinker/relinker/tests/guest_import_fixtures.py index f1b264bf12..45c69cbfb4 100644 --- a/core/relinker/relinker/tests/guest_import_fixtures.py +++ b/core/relinker/relinker/tests/guest_import_fixtures.py @@ -1,8 +1,29 @@ +import contextlib +import shutil import struct +import tempfile +import time from test_guest_intel_trampolines import main_fixture +@contextlib.contextmanager +def fixture_directory(prefix): + directory = tempfile.mkdtemp(prefix=prefix) + try: + yield directory + finally: + deadline = time.monotonic() + 10 + while True: + try: + shutil.rmtree(directory) + break + except PermissionError: + if time.monotonic() >= deadline: + raise + time.sleep(0.05) + + def dynamic(image, offset, phoff, tags): for index, tag in enumerate(tags + [(0, 0)]): struct.pack_into('= 1 else 0 + resource.setrlimit(resource.RLIMIT_CORE, (limit, limit)) + + +def run_without_core_dump(command, **options): + return subprocess.run(command, preexec_fn=without_core_dump, **options) diff --git a/core/relinker/relinker/tests/test_guest_module_identity.py b/core/relinker/relinker/tests/test_guest_module_identity.py index b7f8c48d1d..68e60adc56 100644 --- a/core/relinker/relinker/tests/test_guest_module_identity.py +++ b/core/relinker/relinker/tests/test_guest_module_identity.py @@ -3,9 +3,9 @@ import os import struct import subprocess import sys -import tempfile from test_windows_import_modules import consumer, dynamic, executable, provider +from guest_import_fixtures import fixture_directory from test_guest_module_directories import needed_libraries @@ -37,7 +37,7 @@ def sce_strings(image): def main(): relinker = Path(sys.argv[1]).resolve() - with tempfile.TemporaryDirectory(prefix='anyps5-module-identity-') as directory: + with fixture_directory('anyps5-module-identity-') as directory: work = Path(directory) def convert(label, windows, request, module_name='libGuest', providers=None, guest_request=None, symbol='shared#A#B', module_directory='sce_module', extra=None): diff --git a/core/relinker/relinker/tests/test_guest_unused_import_dependencies.py b/core/relinker/relinker/tests/test_guest_unused_import_dependencies.py new file mode 100644 index 0000000000..f3054ee7bd --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_unused_import_dependencies.py @@ -0,0 +1,119 @@ +from pathlib import Path +import os +import shutil +import struct +import subprocess +import sys +import tempfile + +from guest_import_fixtures import dynamic +from test_guest_deferred_dependencies import page_aligned +from test_guest_intel_trampolines import main_fixture +from test_guest_module_args import args_module +from test_guest_module_directories import module_with_symbol, needed_libraries +from test_guest_module_dir import pe_export_names +from test_guest_needed_modules import module_with_needed +from test_guest_symbol_names import dynamic_tags +from test_macos_output import MachO + + +def consumer(kind): + image = module_with_symbol(False) + tags = dynamic_tags(image) + if kind in ('unreferenced', 'weak-unreferenced', 'needed'): + tags[8] = 0 + elif kind == 'noop': + struct.pack_into('> 32 for _, info, _ in actual) + return + pointers = local + plt if guest else local + if target == "windows": + assert data.startswith(b"MZ") + sections = pe_sections(data) + code = next(section for section in sections if section[0] == b".elf0") + writable = next(section for section in sections if section[0] == (b".elf1" if guest else b".elf0")) + header, = struct.unpack_from(" && std::is_trivially_copyable_v
&& sizeof(Header) == 16); static_assert(std::is_standard_layout_v && std::is_trivially_copyable_v && sizeof(Range) == 32); static_assert(offsetof(Range, deviceAddress) == 16 && offsetof(Range, permissions) == 24); static_assert(std::is_standard_layout_v && std::is_trivially_copyable_v && sizeof(Fault) == 32); -static_assert(offsetof(Fault, address) == 8 && offsetof(Fault, instruction) == 24); +static_assert(offsetof(Fault, address) == 8 && offsetof(Fault, instruction) == 24 && offsetof(Fault, otherFaults) == 28); inline constexpr std::uint32_t WrittenPageShift = 12; inline constexpr std::uint32_t WrittenChunkShift = 17; diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 7d3750a196..01ffcb201c 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -238,6 +238,7 @@ function(add_shader_recompiler target runtimeTarget) HashBytes LayoutBarycentricEmulation MeshBarycentricLayoutOf + NativeCachePath_nid_no_patch ImageTableAbi::PoisonCode ImageTableAbi::PoisonReasonName ImageTableAbi::TableHeader diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index c58096e959..0af4414f5a 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -196,6 +196,8 @@ bool addsImmediate(const RdnaInstruction& instruction, std::uint32_t reg, std::u (isRegister(instruction.source1, RdnaOperandKind::ScalarRegister, reg) && isImmediate(instruction.source0, immediate))); } +bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer, std::uint32_t waveSize = 0u, std::uint32_t skipBranchesPast = 0u); + bool resolveJumpTable(const RdnaProgram& program, std::uint32_t index, BoundedJumpTable& result) { if (index < 9u) return false; const auto& branch = program.instructions[index]; @@ -224,8 +226,13 @@ bool resolveJumpTable(const RdnaProgram& program, std::uint32_t index, BoundedJu const auto& basePc = program.instructions[loadIndex - 3u]; const auto& baseLow = program.instructions[loadIndex - 2u]; const auto& baseHigh = program.instructions[loadIndex - 1u]; - const auto& shift = program.instructions[loadIndex - 4u]; - const auto& bound = program.instructions[loadIndex - 5u]; + const auto offsetReg = scalarIndex(load.source1); + std::uint32_t shiftIndex = 0, boundIndex = 0; + if (offsetReg == NoScalarRegister || + !findLastWriter(program, loadIndex - 3u, {offsetReg}, shiftIndex) || + !findLastWriter(program, shiftIndex, {offsetReg}, boundIndex)) return false; + const auto& shift = program.instructions[shiftIndex]; + const auto& bound = program.instructions[boundIndex]; const auto baseReg = load.source0.reg; std::uint32_t displacement = 0, carry = 0, shiftAmount = 0, maximum = 0; if (basePc.op != RdnaOpcode::SGetpcB64 || !isRegister(basePc.destination, RdnaOperandKind::ScalarRegister, baseReg) || @@ -295,7 +302,7 @@ bool writesScalar(const RdnaInstruction& instruction, std::uint32_t index, std:: return covers(instruction.destination) || covers(instruction.destination2); } -bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer, std::uint32_t waveSize = 0u, std::uint32_t skipBranchesPast = 0u) { +bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer, std::uint32_t waveSize, std::uint32_t skipBranchesPast) { for (auto position = end; position-- > 0u;) { const auto& instruction = program.instructions[position]; const bool skipped = skipBranchesPast != 0u && IsDirectBranchOpcode(instruction.op) && instruction.op != RdnaOpcode::SBranch && instruction.branchTarget > skipBranchesPast; diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index b88053bd7e..1511315ce1 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -83,6 +83,147 @@ bool isLaneIndex(const IrValue& value) { return value.Opcode() == IrOpcode::LaneId; } +std::uint32_t possibleBits(const IrValue& value, unsigned depth = 0) { + if (value.HasImmediate() && value.Type() == IrType::U32) return value.ImmediateU32(); + if (depth >= 8u || value.Type() != IrType::U32) return UINT32_MAX; + const auto arg = [&](std::size_t index) -> const IrValue& { return *value.Argument(index)->Resolve(); }; + const auto immediate = [&](std::size_t index) { return arg(index).HasImmediate() && arg(index).Type() == IrType::U32; }; + switch (value.Opcode()) { + case IrOpcode::BitwiseAnd32: return possibleBits(arg(0), depth + 1) & possibleBits(arg(1), depth + 1); + case IrOpcode::BitwiseOr32: + case IrOpcode::BitwiseXor32: return possibleBits(arg(0), depth + 1) | possibleBits(arg(1), depth + 1); + case IrOpcode::BitFieldUExtract: + if (immediate(2) && arg(2).ImmediateU32() < 32u) return (std::uint32_t{1} << arg(2).ImmediateU32()) - 1u; + return UINT32_MAX; + case IrOpcode::ShiftLeftLogical32: + if (immediate(1) && arg(1).ImmediateU32() < 32u) return possibleBits(arg(0), depth + 1) << arg(1).ImmediateU32(); + return UINT32_MAX; + case IrOpcode::ShiftRightLogical32: + if (immediate(1) && arg(1).ImmediateU32() < 32u) return possibleBits(arg(0), depth + 1) >> arg(1).ImmediateU32(); + return UINT32_MAX; + case IrOpcode::ConvertU32U16: return 0xffffu; + case IrOpcode::ConvertU32U8: return 0xffu; + case IrOpcode::SelectU32: return possibleBits(arg(1), depth + 1) | possibleBits(arg(2), depth + 1); + default: return UINT32_MAX; + } +} + +bool readThroughUnusedBits(IrValue& value, std::size_t index, std::uint32_t demanded) { + bool changed = false; + for (unsigned step = 0; step < 16u; ++step) { + auto& operand = resolveArg(value, index); + if (operand.Type() != IrType::U32) break; + IrValue* next = nullptr; + for (std::size_t side = 0; side < 2u && next == nullptr; ++side) { + if (operand.Opcode() == IrOpcode::BitwiseAnd32) { + auto& mask = resolveArg(operand, side); + if (isImmediate(mask, IrType::U32) && (demanded & ~mask.ImmediateU32()) == 0u) next = &resolveArg(operand, 1u - side); + } else if (operand.Opcode() == IrOpcode::BitwiseOr32 || operand.Opcode() == IrOpcode::BitwiseXor32) { + if ((possibleBits(resolveArg(operand, side)) & demanded) == 0u) next = &resolveArg(operand, 1u - side); + } + } + if (next == nullptr) break; + value.ReplaceArgument(index, next); + changed = true; + } + return changed; +} + +bool maskedPart(IrValue& value, IrValue*& source, std::uint32_t& mask) { + const auto immediate = [&](IrValue& owner, std::size_t index, std::uint32_t& result) { + auto& argument = resolveArg(owner, index); + if (!isImmediate(argument, IrType::U32)) return false; + result = argument.ImmediateU32(); + return true; + }; + std::uint32_t first = 0; + std::uint32_t second = 0; + switch (value.Opcode()) { + case IrOpcode::BitwiseAnd32: + for (std::size_t side = 0; side < 2u; ++side) { + if (immediate(value, side, mask)) { + source = &resolveArg(value, 1u - side); + return !isImmediate(*source, IrType::U32); + } + } + return false; + case IrOpcode::BitFieldUExtract: + if (!immediate(value, 1, first) || !immediate(value, 2, second) || first != 0u || second == 0u || second >= 32u) return false; + source = &resolveArg(value, 0); + mask = (std::uint32_t{1} << second) - 1u; + return true; + case IrOpcode::ShiftLeftLogical32: { + if (!immediate(value, 1, first) || first == 0u || first >= 32u) return false; + auto& inner = resolveArg(value, 0); + std::uint32_t offset = 0; + std::uint32_t count = 0; + if (inner.Opcode() == IrOpcode::BitFieldUExtract && immediate(inner, 1, offset) && immediate(inner, 2, count) && offset == first && count > 0u && count <= 32u - offset) { + source = &resolveArg(inner, 0); + mask = (count == 32u - offset ? UINT32_MAX : (std::uint32_t{1} << (offset + count)) - 1u) & (UINT32_MAX << offset); + return true; + } + if (inner.Opcode() == IrOpcode::ShiftRightLogical32 && immediate(inner, 1, offset) && offset == first) { + source = &resolveArg(inner, 0); + mask = UINT32_MAX << first; + return true; + } + return false; + } + default: return false; + } +} + +bool mergeMaskedParts(IrProgram& program, IrValue& value) { + IrValue* left = nullptr; + IrValue* right = nullptr; + std::uint32_t leftMask = 0; + std::uint32_t rightMask = 0; + if (!maskedPart(resolveArg(value, 0), left, leftMask) || !maskedPart(resolveArg(value, 1), right, rightMask) || left != right) return false; + if ((leftMask | rightMask) == UINT32_MAX) { + replaceWith(value, *left); + return true; + } + if (value.Opcode() != IrOpcode::BitwiseOr32) return false; + IrBuilder builder(program); + value.ReplaceOpcode(IrOpcode::BitwiseAnd32); + value.ReplaceArgument(0, left); + value.ReplaceArgument(1, &builder.Constant(leftMask | rightMask)); + return true; +} + +bool foldRedundantMask(IrValue& value) { + for (std::size_t side = 0; side < 2u; ++side) { + auto& mask = resolveArg(value, side); + auto& operand = resolveArg(value, 1u - side); + if (!isImmediate(mask, IrType::U32)) continue; + const auto keep = mask.ImmediateU32(); + if ((possibleBits(operand) & ~keep) == 0u) { + replaceWith(value, operand); + return true; + } + if (operand.Opcode() == IrOpcode::BitwiseAnd32) { + for (std::size_t part = 0; part < 2u; ++part) { + auto& inner = resolveArg(operand, part); + if (isImmediate(inner, IrType::U32) && (keep & ~inner.ImmediateU32()) == 0u) { + value.ReplaceArgument(1u - side, &resolveArg(operand, 1u - part)); + foldRedundantMask(value); + return true; + } + } + } + if (operand.Opcode() == IrOpcode::BitwiseOr32) { + for (std::size_t part = 0; part < 2u; ++part) { + if ((possibleBits(resolveArg(operand, part)) & keep) == 0u) { + value.ReplaceArgument(1u - side, &resolveArg(operand, 1u - part)); + foldRedundantMask(value); + return true; + } + } + } + } + return false; +} + bool isImmediateU32(const IrValue& value, std::uint32_t immediate) { return isImmediate(value, IrType::U32) && value.ImmediateU32() == immediate; } @@ -738,6 +879,20 @@ bool tryFoldValue(IrProgram& program, IrValue& value, OperandFacts& facts) { } case IrOpcode::BitFieldUExtract: case IrOpcode::BitFieldSExtract: { + if (isImmediate(resolveArg(value, 1), IrType::U32) && isImmediate(resolveArg(value, 2), IrType::U32)) { + const auto fieldOffset = resolveArg(value, 1).ImmediateU32(); + const auto fieldCount = resolveArg(value, 2).ImmediateU32(); + if (fieldCount > 0u && fieldCount < 32u && fieldOffset < 32u && fieldCount <= 32u - fieldOffset) readThroughUnusedBits(value, 0, ((std::uint32_t{1} << fieldCount) - 1u) << fieldOffset); + } + if (resolveArg(value, 0).Opcode() == IrOpcode::ShiftLeftLogical32 && isImmediate(resolveArg(value, 1), IrType::U32) && isImmediate(resolveArg(value, 2), IrType::U32)) { + auto& shifted = resolveArg(value, 0); + auto& amount = resolveArg(shifted, 1); + const auto fieldOffset = resolveArg(value, 1).ImmediateU32(); + if (isImmediate(amount, IrType::U32) && amount.ImmediateU32() > 0u && amount.ImmediateU32() < 32u && fieldOffset >= amount.ImmediateU32() && fieldOffset < 32u) { + value.ReplaceArgument(1, &builder.Constant(fieldOffset - amount.ImmediateU32())); + value.ReplaceArgument(0, &resolveArg(shifted, 0)); + } + } auto& source = resolveArg(value, 0); auto& offset = resolveArg(value, 1); auto& count = resolveArg(value, 2); @@ -748,6 +903,10 @@ bool tryFoldValue(IrProgram& program, IrValue& value, OperandFacts& facts) { return true; } } + if (value.Opcode() == IrOpcode::BitFieldUExtract && isImmediateU32(offset, 0u) && isImmediate(count, IrType::U32) && count.ImmediateU32() < 32u && !isImmediate(source, IrType::U32) && (possibleBits(source) >> count.ImmediateU32()) == 0u) { + replaceWith(value, source); + return true; + } if (!isImmediate(source, IrType::U32) || !isImmediate(offset, IrType::U32) || !isImmediate(count, IrType::U32) || offset.ImmediateU32() > 32u || count.ImmediateU32() > 32u - offset.ImmediateU32()) { return false; } @@ -770,6 +929,7 @@ bool tryFoldValue(IrProgram& program, IrValue& value, OperandFacts& facts) { case IrOpcode::BitCastU32F32: return foldBitCast(builder, value, IrOpcode::BitCastF32U32); case IrOpcode::BitCastF32U32: return foldBitCast(builder, value, IrOpcode::BitCastU32F32); case IrOpcode::ConvertU16U32: { + readThroughUnusedBits(value, 0, 0xffffu); auto& operand = resolveArg(value, 0); if (!isImmediate(operand, IrType::U32)) { return false; @@ -907,6 +1067,7 @@ bool tryFoldValue(IrProgram& program, IrValue& value, OperandFacts& facts) { if (foldU32(builder, value, [](std::uint32_t a, std::uint32_t b) { return a << (b & 31u); })) { return true; } + if (isImmediate(resolveArg(value, 1), IrType::U32)) readThroughUnusedBits(value, 0, UINT32_MAX >> (resolveArg(value, 1).ImmediateU32() & 31u)); auto& shift = resolveArg(value, 1); if (isImmediate(shift, IrType::U32) && (shift.ImmediateU32() & 31u) == 0u) { replaceWith(value, resolveArg(value, 0)); @@ -923,9 +1084,26 @@ bool tryFoldValue(IrProgram& program, IrValue& value, OperandFacts& facts) { replaceWith(value, resolveArg(value, 0)); return true; } + if (!isImmediate(shift, IrType::U32)) return false; + const auto amount = shift.ImmediateU32() & 31u; + readThroughUnusedBits(value, 0, UINT32_MAX << amount); + auto& shifted = resolveArg(value, 0); + if (shifted.Opcode() == IrOpcode::ShiftLeftLogical32 && isImmediate(resolveArg(shifted, 1), IrType::U32) && (resolveArg(shifted, 1).ImmediateU32() & 31u) == amount) { + auto& inner = resolveArg(shifted, 0); + if ((possibleBits(inner) & ~(UINT32_MAX >> amount)) == 0u) { + replaceWith(value, inner); + return true; + } + value.ReplaceOpcode(IrOpcode::BitwiseAnd32); + value.ReplaceArgument(0, &inner); + value.ReplaceArgument(1, &builder.Constant(UINT32_MAX >> amount)); + foldRedundantMask(value); + return true; + } return false; } case IrOpcode::ShiftRightArithmetic32: { + if (isImmediate(resolveArg(value, 1), IrType::U32)) readThroughUnusedBits(value, 0, UINT32_MAX << (resolveArg(value, 1).ImmediateU32() & 31u)); return foldU32(builder, value, [](std::uint32_t a, std::uint32_t b) { return static_cast(std::bit_cast(a) >> (b & 31u)); }); } case IrOpcode::ShiftLeftLogical64: { @@ -957,7 +1135,14 @@ bool tryFoldValue(IrProgram& program, IrValue& value, OperandFacts& facts) { if (foldU32(builder, value, [](std::uint32_t a, std::uint32_t b) { return a & b; })) { return true; } - return replaceBinaryIdentity(value, IrType::U32, 0xffffffffu); + if (replaceBinaryIdentity(value, IrType::U32, 0xffffffffu)) return true; + if (foldRedundantMask(value)) return true; + bool changed = false; + for (std::size_t side = 0; side < 2u; ++side) { + if (isImmediate(resolveArg(value, side), IrType::U32)) changed |= readThroughUnusedBits(value, 1u - side, resolveArg(value, side).ImmediateU32()); + } + if (changed) foldRedundantMask(value); + return changed; } case IrOpcode::BitwiseAnd64: { if (foldU64(builder, value, [](std::uint64_t a, std::uint64_t b) { return a & b; })) { @@ -971,7 +1156,8 @@ bool tryFoldValue(IrProgram& program, IrValue& value, OperandFacts& facts) { if (foldU32(builder, value, [opcode](std::uint32_t a, std::uint32_t b) { return opcode == IrOpcode::BitwiseOr32 ? a | b : a ^ b; })) { return true; } - return replaceBinaryIdentity(value, IrType::U32, 0u); + if (replaceBinaryIdentity(value, IrType::U32, 0u)) return true; + return opcode == IrOpcode::BitwiseOr32 && mergeMaskedParts(program, value); } case IrOpcode::BitwiseNot32: { auto& operand = resolveArg(value, 0); diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 79119bf55d..208f11d4f0 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -364,7 +364,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l std::vector samplerIntegerModes(info.samplers.size(), 0u); std::vector samplerFloatModes(info.samplers.size(), 0u); std::vector directSamplerModes(info.samplers.size(), 0u); - const auto samplerMode = [](const ImageResource& image) { return !image.constantSwizzle && (image.numericClass == IrTextureNumericClass::Sint || image.conversionFormat != IrBufferFormat::Invalid || image.depthBits) ? 2u : 1u; }; + const auto samplerMode = [](const ImageResource& image) { return !image.constantSwizzle && (image.numericClass == IrTextureNumericClass::Uint || image.numericClass == IrTextureNumericClass::Sint || image.conversionFormat != IrBufferFormat::Invalid || image.depthBits) ? 2u : 1u; }; const auto pairSampler = [&](std::uint32_t sampler, const ImageResource& image) { const auto mode = samplerMode(image); samplerModes.at(sampler) |= mode; diff --git a/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp b/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp index a580617abd..6e8c4e269d 100644 --- a/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp @@ -1,7 +1,9 @@ #include "Optimization/RedundantValueEliminator.hpp" +#include #include #include #include +#include #include #include #include @@ -134,31 +136,38 @@ struct Operand { bool operator==(const Operand& other) const = default; }; +Operand OperandOf(const IrValue* resolved) { + if (resolved->Parent() == nullptr && resolved->HasImmediate()) return {nullptr, resolved->Type(), resolved->ImmediateU64()}; + return {resolved, resolved->Type(), 0u}; +} + struct ValueKey { IrOpcode opcode; IrType type; std::uint64_t flags; - std::vector operands; + std::array operands{}; + std::size_t operandCount = 0; - bool operator==(const ValueKey& other) const = default; + bool operator==(const ValueKey& other) const { + if (opcode != other.opcode || type != other.type || flags != other.flags || operandCount != other.operandCount) return false; + for (std::size_t index = 0; index < operandCount; ++index) { + if (operands[index] != other.operands[index] && OperandOf(operands[index]) != OperandOf(other.operands[index])) return false; + } + return true; + } }; struct ValueKeyHash { std::size_t operator()(const ValueKey& key) const { std::size_t hash = static_cast(key.opcode) * 0x9e3779b97f4a7c15ull ^ static_cast(key.type) ^ std::hash{}(key.flags) * 31u; - for (const auto& operand : key.operands) { + for (const auto* argument : std::span(key.operands).first(key.operandCount)) { + const auto operand = OperandOf(argument); hash = hash * 1099511628211ull ^ std::hash{}(operand.value) ^ std::hash{}(operand.immediate) * 7u ^ static_cast(operand.type); } return hash; } }; -Operand OperandOf(const IrValue* argument) { - const IrValue* resolved = argument->Resolve(); - if (resolved->Parent() == nullptr && resolved->HasImmediate()) return {nullptr, resolved->Type(), resolved->ImmediateU64()}; - return {resolved, resolved->Type(), 0u}; -} - std::vector ReversePostOrder(IrBlock& entry) { std::vector order; std::unordered_map visited; @@ -216,15 +225,13 @@ RedundantValueEliminationStats RedundantValueEliminator::Eliminate(IrProgram& pr std::unordered_map available; std::vector added; std::unordered_map replacements; - ValueKey key; const auto visit = [&](IrBlock& block) { for (auto* inst : block.Instructions()) { if (!Recomputable(inst->Opcode()) || inst->HasImmediate() || !inst->HasUses()) continue; - key.opcode = inst->Opcode(); - key.type = inst->Type(); - key.flags = inst->Flags(); - key.operands.clear(); - for (std::size_t index = 0; index < inst->ArgumentCount(); ++index) key.operands.push_back(OperandOf(inst->Argument(index))); + ValueKey key{inst->Opcode(), inst->Type(), inst->Flags()}; + key.operandCount = inst->ArgumentCount(); + if (key.operandCount > key.operands.size()) continue; + for (std::size_t index = 0; index < key.operandCount; ++index) key.operands[index] = inst->Argument(index)->Resolve(); const auto [found, inserted] = available.try_emplace(key, inst); if (inserted) { added.push_back(&found->first); diff --git a/core/shader/recompiler/ShaderCacheDirectory.hpp b/core/shader/recompiler/ShaderCacheDirectory.hpp index 84fbd1ae36..9b3893ce2a 100644 --- a/core/shader/recompiler/ShaderCacheDirectory.hpp +++ b/core/shader/recompiler/ShaderCacheDirectory.hpp @@ -33,18 +33,42 @@ namespace ShaderRecompiler { std::filesystem::path ShaderCacheDirectory(); +#ifdef _WIN32 +inline std::filesystem::path NativeCachePath_nid_no_patch(const std::filesystem::path& path, std::error_code& error) { + auto native = path; + native.make_preferred(); + if (native.native().starts_with(L"\\\\?\\") || native.native().starts_with(L"\\\\.\\")) return native; + const auto capacity = GetFullPathNameW(native.c_str(), 0, nullptr, nullptr); + if (capacity == 0) { error = std::error_code(GetLastError(), std::system_category()); return {}; } + std::wstring absolute(capacity, L'\0'); + const auto size = GetFullPathNameW(native.c_str(), capacity, absolute.data(), nullptr); + if (size == 0 || size >= capacity) { + error = std::error_code(size == 0 ? GetLastError() : ERROR_INSUFFICIENT_BUFFER, std::system_category()); + return {}; + } + absolute.resize(size); + if (absolute.starts_with(L"\\\\")) return std::filesystem::path(L"\\\\?\\UNC\\" + absolute.substr(2)); + return std::filesystem::path(L"\\\\?\\" + absolute); +} +#endif + inline bool WriteFileAtomically(const std::filesystem::path& path, std::span bytes) { std::error_code error; - std::filesystem::create_directories(path.parent_path(), error); +#ifdef _WIN32 + const auto destination = NativeCachePath_nid_no_patch(path, error); + if (error) return false; +#else + const auto& destination = path; +#endif + std::filesystem::create_directories(destination.parent_path(), error); static std::atomic serial{0}; const auto unique = std::hash{}(std::this_thread::get_id()) ^ static_cast(std::chrono::steady_clock::now().time_since_epoch().count()); - auto temporary = path; + auto temporary = destination; temporary += ".tmp." + std::to_string(unique) + "." + std::to_string(serial.fetch_add(1, std::memory_order_relaxed)); #ifdef _WIN32 struct RenameInfo { DWORD flags; HANDLE root; DWORD nameBytes; WCHAR name[1]; }; static_assert(sizeof(RenameInfo) == sizeof(FILE_RENAME_INFO) && offsetof(RenameInfo, name) == offsetof(FILE_RENAME_INFO, FileName)); - const auto destination = std::filesystem::absolute(path, error); - if (error || destination.native().size() > (MAXDWORD - sizeof(RenameInfo)) / sizeof(wchar_t)) return false; + if (destination.native().size() > (MAXDWORD - sizeof(RenameInfo)) / sizeof(wchar_t)) return false; const auto nameBytes = destination.native().size() * sizeof(wchar_t); std::vector renameBuffer(sizeof(RenameInfo) + nameBytes); auto* rename = reinterpret_cast(renameBuffer.data()); @@ -89,7 +113,7 @@ inline bool WriteFileAtomically(const std::filesystem::path& path, std::span PrebuiltCachePaths(const std::filesystem::pat CachePack::CachePack(const std::filesystem::path& path) { static_assert(std::endian::native == std::endian::little); #ifdef _WIN32 - const auto handle = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + std::error_code pathError; + const auto filename = NativeCachePath_nid_no_patch(path, pathError); + if (pathError) { error = "cannot resolve"; return; } + const auto handle = CreateFileW(filename.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (handle == INVALID_HANDLE_VALUE) { error = "cannot open"; return; } LARGE_INTEGER size{}; if (!GetFileSizeEx(handle, &size) || size.QuadPart < static_cast(sizeof(PackHeader)) || static_cast(size.QuadPart) > std::numeric_limits::max()) { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 31815e3fb6..3a3d947b8c 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -195,7 +195,10 @@ private: std::shared_ptr MapFile(const std::filesystem::path& path) { #ifdef _WIN32 - const HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + std::error_code error; + const auto filename = NativeCachePath_nid_no_patch(path, error); + if (error) return nullptr; + const HANDLE file = CreateFileW(filename.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) return nullptr; LARGE_INTEGER length{}; if (!GetFileSizeEx(file, &length) || length.QuadPart <= 0 || static_cast(length.QuadPart) > std::numeric_limits::max()) { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp index 73ebbf6a78..ee435ffc67 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp @@ -25,6 +25,8 @@ struct SpirvRequirements { bool functionScratch = false; bool pixelValidMask = false; bool bufferInt64Atomics = false; + bool coherentBufferPairs = false; + bool bufferU64View = false; bool imageInt64Atomics = false; bool sharedInt64Atomics = false; bool float64 = false; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 0c97974dd0..6514d92cc1 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -376,6 +376,8 @@ SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program) { } const auto& memory = program.Resources().memoryInfo.at(memoryIndex); requirements.coherentBuffers = requirements.coherentBuffers || (memory.coherent && !memory.gpuDescriptor); + const bool pair = inst->Opcode() == IrOpcode::LoadBufferU32x2 || inst->Opcode() == IrOpcode::StoreBufferU32x2; + requirements.coherentBufferPairs = requirements.coherentBufferPairs || (pair && memory.coherent && !memory.gpuDescriptor); if (!memory.planningOnly) { requirements.subgroupLocalInvocationId = requirements.subgroupLocalInvocationId || program.Resources().stage == IrShaderStage::Compute; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp index 54203f17de..53f65c7c1c 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp @@ -2,6 +2,7 @@ #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" #include +#include #include namespace ShaderRecompiler { @@ -51,6 +52,10 @@ void DefineBdaFaultFunction(SpirvEmitterState& state) { const auto previous = state.module.AllocateId(); const auto scope = ConstantU32(state, spv::ScopeDevice); const auto relaxed = ConstantU32(state, spv::MemorySemanticsMaskNone); + const auto notUnaligned = Binary(state, spv::OpINotEqual, TypeBool(state), reason, ConstantU32(state, static_cast(BdaAbi::FaultReason::Unaligned))); + EmitIfCondition(state, notUnaligned, [&] { + state.module.AddFunction(spv::OpAtomicOr, u32, state.module.AllocateId(), BdaWord(state, state.faultBufferVariable, ConstantU32(state, static_cast(offsetof(BdaAbi::Fault, otherFaults) / sizeof(std::uint32_t)))), scope, relaxed, ConstantU32(state, 1u)); + }); state.module.AddFunction(spv::OpAtomicCompareExchange, TypeU32(state), previous, pointer, scope, relaxed, relaxed, ConstantU32(state, static_cast(BdaAbi::FaultState::Writing)), ConstantU32(state, 0u)); const auto won = Binary(state, spv::OpIEqual, TypeBool(state), previous, ConstantU32(state, 0u)); EmitIfCondition(state, won, [&] { @@ -63,7 +68,6 @@ void DefineBdaFaultFunction(SpirvEmitterState& state) { store(4, bytes); store(5, ConstantU32(state, static_cast(state.program.Resources().stage))); store(6, instruction); - store(7, ConstantU32(state, 0u)); const auto release = ConstantU32(state, spv::MemorySemanticsReleaseMask | spv::MemorySemanticsUniformMemoryMask); state.module.AddFunction(spv::OpAtomicStore, pointer, scope, release, ConstantU32(state, static_cast(BdaAbi::FaultState::Ready))); }); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 65e705b3e9..ca0506ad1a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -18,8 +19,14 @@ namespace ShaderRecompiler { namespace { +std::string FailureText(const IrProgram& program) { + char hash[24]; + std::snprintf(hash, sizeof(hash), "%llx", static_cast(program.Resources().shaderHash)); + return std::string("SPIR-V emission failed: hash=0x") + hash + " stage=" + std::to_string(static_cast(program.Resources().stage)); +} + [[noreturn]] void FailProgram(const IrProgram& program, const char* reason) { - throw std::runtime_error("SPIR-V emission failed: hash=0x" + std::to_string(program.Resources().shaderHash) + " stage=" + std::to_string(static_cast(program.Resources().stage)) + " reason=" + reason); + throw std::runtime_error(FailureText(program) + " reason=" + reason); } const ShaderWorkgroupInputInfo* ShaderWorkgroupInputFor(const SpirvEmitterState& state) { @@ -233,7 +240,7 @@ std::uint32_t SpirvValueEmitContext::Label(const IrBlock* block) const { } [[noreturn]] void SpirvValueEmitContext::Fail(const IrValue& inst, const char* reason) const { - throw std::runtime_error("SPIR-V emission failed: hash=0x" + std::to_string(state.program.Resources().shaderHash) + " stage=" + std::to_string(static_cast(state.program.Resources().stage)) + " opcode=" + std::string(IrOpcodeName(inst.Opcode())) + " reason=" + reason); + throw std::runtime_error(FailureText(state.program) + " opcode=" + std::string(IrOpcodeName(inst.Opcode())) + " reason=" + reason); } std::vector SpirvEmitter::Emit(const IrProgram& program, const CompiledBindingLayout& bindings, const SpirvTargetOptions& target) const { @@ -275,6 +282,8 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.requirements.ldsLock = false; state.requirements.ldsBufferInt64Atomics = true; } + const bool int64Atomics = std::ranges::find(state.supportedCapabilities, static_cast(spv::CapabilityInt64Atomics)) != state.supportedCapabilities.end(); + state.requirements.bufferU64View = state.requirements.bufferInt64Atomics || (state.requirements.coherentBufferPairs && int64Atomics); if (HostSubgroupSingleLane(state)) { state.requirements.subgroupBallot = false; state.requirements.subgroupShuffle = false; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 577211dbde..a8068f0415 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -419,7 +419,7 @@ void DefineOutputs(SpirvEmitterState& state) { case StageOutputKind::StencilRef: if (std::ranges::find(state.supportedCapabilities, static_cast(spv::CapabilityStencilExportEXT)) == state.supportedCapabilities.end()) { const auto values = StencilReferenceValues(state.program, PixelInfo(state).psStencilOperationValueExport ? 8u : 0u); - if (values.empty()) throw std::runtime_error("stencil reference export emulation requires a proven set of at most 16 values"); + if (values.empty()) throw std::runtime_error("stencil reference export emulation found no stencil export"); if (!StencilReferenceDefinedForKeptInvocations(state.program)) throw std::runtime_error("stencil reference export emulation requires a stencil export on every kept path"); if (PixelInfo(state).psEarlyZ || PixelInfo(state).psOrderedPixelShader || !StencilReplaySafe(state.program)) throw std::runtime_error("stencil reference export emulation requires a replay-safe pixel shader with late tests"); state.stencilReplay = true; @@ -479,7 +479,7 @@ void DefineDescriptors(SpirvEmitterState& state) { case DescriptorBindingKind::Buffers: state.storageBufferVariable = Define(ArrayType(StorageBufferBlockType(state)), "buffers"); if (state.requirements.coherentBufferAlias) state.storageBufferCoherentVariable = Define(ArrayType(StorageBufferBlockType(state)), "buffers_coherent"); - if (state.requirements.bufferInt64Atomics) state.storageBufferU64Variable = Define(ArrayType(StorageBufferU64BlockType(state)), "buffers_u64"); + if (state.requirements.bufferU64View) state.storageBufferU64Variable = Define(ArrayType(StorageBufferU64BlockType(state)), "buffers_u64"); if (state.storageBufferU64Variable != 0u || state.storageBufferCoherentVariable != 0u) { for (const auto variable : {state.storageBufferVariable, state.storageBufferCoherentVariable, state.storageBufferU64Variable}) { if (variable != 0u) state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationAliased); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index bd58498bc8..9bbf4fb15b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -259,6 +259,8 @@ void DefineModule(SpirvEmitterState& state) { if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityInt64Atomics)) == state.supportedCapabilities.end()) { FailEmit("64-bit buffer atomics need shaderBufferInt64Atomics"); } + } + if (state.requirements.bufferU64View) { state.module.EmitCapability(spv::CapabilityInt64); state.module.EmitCapability(spv::CapabilityInt64Atomics); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index fa019cafce..aa7dc2bbe8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -723,6 +723,27 @@ std::uint32_t formattedGpuCall(SpirvValueEmitContext& context, const IrValue& in return value; } +struct CoherentPair { + MemoryResourceAccess wide; + std::uint32_t index; + std::uint32_t usable; +}; + +std::optional coherentPair(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t components) { + auto& state = context.state; + const auto& memory = context.Memory(instruction); + if (components != 2u || !memory.coherent || memory.gpuDescriptor || memory.dataBits != 32u || state.storageBufferU64Variable == 0u) return std::nullopt; + const auto low = address(context, instruction, resource, 0u, 4u); + const auto high = address(context, instruction, resource, 4u, 4u); + const auto wide = directAccess(context, instruction, true); + const auto byte = EmitAddU32(state, low.guest, wide.byteOffset); + const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), byte, ConstantU32(state, 3u)); + const auto aligned = both(state, equal(state, baseMisalignment(context, instruction), ConstantU32(state, 0u)), equal(state, EmitAndConstant(state, byte, 7u), ConstantU32(state, 0u))); + const auto adjacent = equal(state, high.guest, EmitAddU32(state, low.guest, ConstantU32(state, 4u))); + const auto inBounds = both(state, both(state, low.inBounds, high.inBounds), EmitMemoryElementInBounds(state, wide, index)); + return CoherentPair{wide, index, both(state, both(state, aligned, adjacent), inBounds)}; +} + } std::uint32_t EmitRuntimeBufferLoad(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t components) { @@ -742,15 +763,10 @@ std::uint32_t EmitRuntimeBufferLoad(SpirvValueEmitContext& context, const IrValu } return composite(state, components, values); }; - if (components != 2u || !memory.coherent || memory.gpuDescriptor || state.storageBufferU64Variable == 0u) return readComponents(); - const auto runtime = address(context, instruction, resource, 0u, 8u); - const auto wide = directAccess(context, instruction, true); - const auto byte = EmitAddU32(state, runtime.guest, wide.byteOffset); - const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), byte, ConstantU32(state, 3u)); - const auto aligned = both(state, equal(state, baseMisalignment(context, instruction), ConstantU32(state, 0u)), equal(state, EmitAndConstant(state, byte, 7u), ConstantU32(state, 0u))); - const auto inBounds = both(state, runtime.inBounds, EmitMemoryElementInBounds(state, wide, index)); - return EmitValueIfElse(state, both(state, aligned, inBounds), type, [&] { - const auto pointer = EmitStorageBufferElementPointer(state, wide, index, TypeStorageBufferU64ElementPointer(state)); + const auto pair = coherentPair(context, instruction, resource, components); + if (!pair) return readComponents(); + return EmitValueIfElse(state, pair->usable, type, [&] { + const auto pointer = EmitStorageBufferElementPointer(state, pair->wide, pair->index, TypeStorageBufferU64ElementPointer(state)); const auto value = state.module.AllocateId(); state.module.AddFunction(spv::OpAtomicLoad, TypeScalarU64(state), value, pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsMaskNone)); const auto high = Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), value, BdaConstant(state, 32u)); @@ -769,10 +785,25 @@ void EmitRuntimeBufferStore(SpirvValueEmitContext& context, const IrValue& instr return; } const auto values = storeValues(state, storeData(context, instruction), components); - for (std::uint32_t component = 0; component < components; ++component) { - const auto access = address(context, instruction, resource, component * 4u, memory.dataBits / 8u); - EmitIfCondition(state, access.inBounds, [&] { writeBuffer(context, instruction, access.guest, values[component], memory.dataBits); }); + const auto writeComponents = [&] { + for (std::uint32_t component = 0; component < components; ++component) { + const auto access = address(context, instruction, resource, component * 4u, memory.dataBits / 8u); + EmitIfCondition(state, access.inBounds, [&] { writeBuffer(context, instruction, access.guest, values[component], memory.dataBits); }); + } + }; + const auto pair = coherentPair(context, instruction, resource, components); + if (!pair) { + writeComponents(); + return; } + EmitIfCondition(state, pair->usable, [&] { + const auto u64 = TypeScalarU64(state); + const auto high = Binary(state, spv::OpShiftLeftLogical, u64, Unary(state, spv::OpUConvert, u64, values[1]), BdaConstant(state, 32u)); + const auto value = Binary(state, spv::OpBitwiseOr, u64, Unary(state, spv::OpUConvert, u64, values[0]), high); + const auto pointer = EmitStorageBufferElementPointer(state, pair->wide, pair->index, TypeStorageBufferU64ElementPointer(state)); + state.module.AddFunction(spv::OpAtomicStore, pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsMaskNone), value); + }); + EmitIfCondition(state, Unary(state, spv::OpLogicalNot, TypeBool(state), pair->usable), writeComponents); }); } diff --git a/core/shader/recompiler/StencilReplay.cpp b/core/shader/recompiler/StencilReplay.cpp index cd8c8d6416..fae2057358 100644 --- a/core/shader/recompiler/StencilReplay.cpp +++ b/core/shader/recompiler/StencilReplay.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -605,9 +606,12 @@ std::vector StencilReferenceValues(const IrProgram& program, std: if (info.kind != ExportTargetKind::MrtZ || (info.en & 2u) == 0u) continue; std::set visiting; const auto values = Values(program, instruction->Argument(0), visiting, 0u, 1u, instruction->Argument(1)); - if (!values) return {}; + if (!values) { + std::vector all(256u); + std::iota(all.begin(), all.end(), 0u); + return all; + } for (const auto value : *values) result.insert((value >> shift) & 0xffu); - if (result.size() > 16u) return {}; } } return {result.begin(), result.end()}; diff --git a/core/shader/recompiler/tests/CacheFileReadTests.cpp b/core/shader/recompiler/tests/CacheFileReadTests.cpp index 29f5e61b3f..f0e936b798 100644 --- a/core/shader/recompiler/tests/CacheFileReadTests.cpp +++ b/core/shader/recompiler/tests/CacheFileReadTests.cpp @@ -16,11 +16,88 @@ void* operator new[](std::size_t size) { return ::operator new(size); } void operator delete[](void* memory) noexcept { ::operator delete(memory); } void operator delete[](void* memory, std::size_t) noexcept { ::operator delete(memory); } -static void Require(bool ok) { if (!ok) throw std::runtime_error("Cache file read failed"); } +static void Require(bool ok, const char* message = "Cache file read failed") { if (!ok) throw std::runtime_error(message); } + +#ifdef _WIN32 +static void RemoveCachePaths(const std::filesystem::path& path) { + WIN32_FIND_DATAW data{}; + const auto search = FindFirstFileW((path / L"*").c_str(), &data); + if (search != INVALID_HANDLE_VALUE) { + do { + const std::wstring name = data.cFileName; + if (name == L"." || name == L"..") continue; + const auto child = path / name; + if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) RemoveCachePaths(child); + else DeleteFileW(child.c_str()); + } while (FindNextFileW(search, &data)); + FindClose(search); + } + RemoveDirectoryW(path.c_str()); +} + +static void VerifyCachePaths(const std::filesystem::path& root) { + const auto extended = [](const std::filesystem::path& path) { + auto native = path; + native.make_preferred(); + return std::filesystem::path(L"\\\\?\\" + native.native()); + }; + const std::vector original{std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}}; + const std::vector replacement{std::byte{5}, std::byte{6}}; + const auto verify = [&](const std::filesystem::path& path) { + for (const auto& alias : {path, extended(path)}) { + Require(ShaderRecompiler::WriteFileAtomically(alias, original), "cannot write cache path"); + std::vector bytes; + Require(ShaderRecompiler::ReadWholeFile(path, bytes) && bytes == original, "cannot read ordinary cache path"); + Require(ShaderRecompiler::ReadWholeFile(extended(path), bytes) && bytes == original, "cannot read extended cache path"); + const auto old = CreateFileW(extended(path).c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + Require(old != INVALID_HANDLE_VALUE, "cannot retain the original cache reader"); + struct Close { HANDLE file; ~Close() { CloseHandle(file); } } close{old}; + Require(ShaderRecompiler::WriteFileAtomically(alias, replacement), "cannot replace cache path with retained reader"); + Require(ShaderRecompiler::ReadWholeFile(alias, bytes) && bytes == replacement, "replacement cache bytes do not match"); + std::byte retained[4]{}; + DWORD count = 0; + Require(ReadFile(old, retained, sizeof(retained), &count, nullptr) && count == original.size(), "cannot read the retained cache handle"); + Require(std::equal(original.begin(), original.end(), retained), "retained cache bytes changed after replacement"); + } + WIN32_FIND_DATAW data{}; + const auto temporary = FindFirstFileW((extended(path).parent_path() / L"*.tmp.*").c_str(), &data); + if (temporary != INVALID_HANDLE_VALUE) FindClose(temporary); + Require(temporary == INVALID_HANDLE_VALUE && GetLastError() == ERROR_FILE_NOT_FOUND, "cache temporary file was left behind"); + }; + auto directory = root / "paths"; + while (directory.native().size() < 150) directory /= std::wstring(std::min(40, 150 - directory.native().size()), L'd'); + for (const auto length : {180u, 235u, 269u, 320u}) { + const auto size = std::max(length, directory.native().size() + 12); + verify(directory / (std::wstring(size - directory.native().size() - 5, L'x') + L".bin")); + } + auto deep = root; + while (deep.native().size() < 300) deep /= std::wstring(40, L'd'); + verify(deep / L"cache \u00e4\u6f22.bin"); + std::filesystem::create_directories(root / "relative"); + struct RestoreDirectory { + std::filesystem::path previous = std::filesystem::current_path(); + ~RestoreDirectory() { + std::error_code error; + std::filesystem::current_path(previous, error); + } + } restoreDirectory; + std::filesystem::current_path(root); + auto relative = (root / "relative" / ".." / "relative.bin").lexically_relative(std::filesystem::current_path()); + Require(!relative.empty(), "cannot construct the cache relative path"); + Require(ShaderRecompiler::WriteFileAtomically(relative, original), "cannot write the cache relative path"); + std::vector bytes; + Require(ShaderRecompiler::ReadWholeFile(relative, bytes) && bytes == original, "cannot read the cache relative path"); + const auto missing = deep / "missing.bin"; + Require(!ShaderRecompiler::ReadWholeFile(missing, bytes) && bytes.empty(), "missing cache path did not clear the read buffer"); +} +#endif int main() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-cache-read-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + auto directory = path; + directory += "-paths"; int status = 0; try { std::vector bytes{std::byte{1}}; @@ -37,8 +114,15 @@ int main() { FailLargeAllocation = false; Require(!read && failed.empty()); std::cout << "5 cache file read cases passed\n"; +#ifdef _WIN32 + VerifyCachePaths(directory); + std::cout << "Windows cache path and retained-reader replacement cases passed\n"; +#endif } catch (const std::exception& error) { FailLargeAllocation = false; std::cerr << error.what() << '\n'; status = 1; } std::error_code error; std::filesystem::remove(path, error); +#ifdef _WIN32 + RemoveCachePaths(std::filesystem::path(L"\\\\?\\" + directory.native())); +#endif return status; } diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 09f3921608..574244a6da 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -126,6 +126,7 @@ flowchart LR - A stage that `sceAgcDriverResolve*Abi` cannot prepare for the state the title creates its pipeline with (a compute shader that calls user-data pointers, a descriptor the resource plan rejects) is left to its first use like a stage deferred at registration: `ResolveShaderAbi` and `PrepareGraphicsStages` catch the failure inside their own preparation transaction, so the `ShaderPreparationScope` the title's call runs in still commits, log it once per shader, mark the shader deferred and record the ABI key, so a repeated call prepares nothing; `ResolvePreparedGraphics` skips a deferred stage's rectangles until a draw has prepared it. A draw or dispatch that cannot prepare the stage either fails as before. Driver failures, `ProcessShutdown` and a failure stored by the shader's registration preparation still propagate from the ABI call. - A compute shader whose `s_swappc_b64` calls a user-data pointer cannot be linked at registration, because the callee's address is only known at dispatch. `InvocationFor` in [`ShaderRegistry.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) reads each callee from guest memory, appends one copy per call after the caller (`LinkUserDataCalls`) and prepares that linked code, so a different callee gets its own prepared entry. - A draw's `Graphics::Pipeline` comes from `CachedPipeline`. When the device has `VK_EXT_graphics_pipeline_library` with fast linking, `VK_EXT_extended_dynamic_state` and `VK_KHR_dynamic_rendering`, draws render with `vkCmdBeginRenderingKHR` instead of render passes and the pipeline is linked from four libraries (vertex input, pre-rasterization shaders, fragment shader, fragment output) kept per device in [`PipelineLibrary.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp). A library built for one pipeline is reused by the next: another blend, vertex layout or target format builds only that part (the shader libraries name no attachment format), and cull mode, front face, depth and stencil state are dynamic, so changing them only links. The fast link is what the draw waits for; a background thread then links the same libraries with link-time optimization and the pipeline binds that one from the next draw on. Push constants then use a 256-byte block with a 128-byte slot per side: the pre-rasterization stages stack from byte 0, the pixel shader always starts at byte 128 (`StagePushOffset`), so its library does not depend on how much push data its vertex shader uses. Pipelines with per-face depth bias, a separate alpha blend into an 8_8 target with the alternate component order (both draw twice), rect lists, geometry shaders or a stage without a variant id are created whole, still with dynamic rendering; devices without the extensions (or with `APS5_NO_GPL=1`) keep render passes and whole pipelines. +- A storage or sampled view of a thin array whose whole surface has more layers than the device's `maxImageArrayLayers` (2048 on NVIDIA; a PS5 array may hold up to 8192) is served from its viewed slices alone: `ArrayLayerWindow` in [`TextureTiling.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) describes them as an array of their own starting at the first viewed slice, which is exact because a non-XOR swizzle does not depend on the slice index. A 3D volume is not an array (its depth is bounded by `maxImageDimension3D`) and a 2D image is one layer whatever its last array field holds; both pass unchanged. A view whose slices alone exceed the limit, a view of an XOR swizzle that mixes in the slice index, and any other image past the limit throw; a sampled lookup (`cachedTexture`) throws before it reads or snapshots the surface. - A draw whose `PA_SC_AA_CONFIG` selects 2, 4 or 8 samples renders into host-only multisampled images. [`MultisampleTarget.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) keeps one image per device, surface address, format, extent and sample count (`MultisampleTargetView`); the pipeline, render pass and library keys carry `State::samples`, and a multisampled depth target is a multisampled depth surface. The guest sample planes and FMASK are never read or written, so the registry also guards the target's guest range: a CPU read (`GuestMemory::SetReadGuard`), a DMA copy, and a sampled or storage image read of it throw. A `CB_COLOR_CONTROL` resolve mode over a multisampled CB0 runs as a render pass whose resolve attachment is CB1's resident image (`ResolveMultisampleTarget`). A zero fill of the target's CMASK (`NoteColorMetadataFill`, from fill kernels and CP DMA fills) clears the image at its next use. - A texture over a depth surface samples the depth image itself when it views one 2D slice of the plane's own format (`DepthSurfaceTexture` in [`DepthSurface.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp)). A view of several slices (a 2D array or a cube) or an integer view of a D16 plane is served by a `DepthPlaneCopy`, an image the slices are copied into at every use: from the depth surface bound at each slice's address, else from guest memory. Such a texture is never kept in the resource templates (`RefreshedPerUse`). - Guest memory reaches the GPU through host imports (`VK_EXT_external_memory_host`) where the driver accepts them, and through copies otherwise ([`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). A registered range larger than the import window (64 MiB) is imported per window for buffer and fill accesses and whole for an address-based build. Live imports stay under a budget, with the least recently used import retired first (see TechnicalDebt, Host import windows and budget). diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 3d1e5fd416..b1b725a14f 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -12,7 +12,7 @@ ctest --test-dir build-relinker --output-on-failure Python must be found during CMake configuration for Python tests to be registered. List registered tests with `ctest --test-dir build-relinker -N` and look for `input_magic`, a Python-backed relinker test. If automatic discovery fails, add `-DPython3_EXECUTABLE="C:/path/to/python.exe"` to the configure command, using the path to your Python interpreter, then list the tests again. -This mode builds the conversion tool and its tests on Linux, Windows and macOS, including Apple Silicon. macOS uses AppleClang from the Xcode command-line tools; Windows uses the MinGW-w64 toolchain below. The executable is `build-relinker/core/relinker/relinker` (`relinker.exe` on Windows with Ninja). +This mode builds the conversion tool and its tests on Linux, Windows and macOS, including Apple Silicon. macOS uses AppleClang from the Xcode command-line tools. Windows uses MinGW-w64 GCC with C++20 support and accepts distributions with static or shared runtimes; the WinLibs requirement below applies to the full build. The executable is `build-relinker/core/relinker/relinker` (`relinker.exe` on Windows with Ninja). The output is x86-64: Linux ELF by default, Windows PE with `--windows` or Mach-O with `--macos` ([usage](../user/USAGE.md#input-and-conversion)). Converted games need system libraries built for the target OS and a compatible x86-64 host. This mode does not build those libraries, and releases ship them for Linux and Windows only. Tests inspect all three output formats; execution checks run only on their compatible hosts. @@ -35,6 +35,14 @@ git submodule update --init --recursive - Bitdefender Advanced Threat Defense hooks Windows calls that `sceKernelRaiseException` makes while its target thread is suspended. When the target is suspended inside ntdll's exception dispatch while it holds an ntdll lock, the hook waits for that lock and `guest_raise_exception` hangs until its timeout. A scanning exception for the build directory does not prevent it; add `guest_raise_exception_tests.exe` to the Advanced Threat Defense exceptions. - FFmpeg binaries are downloaded during configuration unless `FFMPEG_PREBUILT_DIR` is set. With the WinLibs CMake, the download fails with status 60 (`SSL peer certificate or SSH remote key was not OK`) unless `SSL_CERT_FILE` names a CA bundle, for example `C:\Program Files\Git\ucrt64\etc\ssl\certs\ca-bundle.crt` from Git for Windows 2.56 and later (`mingw64` instead of `ucrt64` before 2.56), as in CI. +## Web converter releases + +The Release workflow builds the web converter and the Linux and Windows libraries from the same commit. It tests the converter and publishes its frontend and WASM files as `web-.zip`, with the tag and commit recorded in `web-release.json`. + +Pages downloads the converter and both library archives from one release selected through GitHub's latest-release API. `release.json` records the same tag and commit. Missing, corrupt or mismatched assets fail the Pages job before deployment. Releases made before the web asset was added need a new release; Pages does not combine them with a converter built from `main`. + +Changes to the converter or its frontend reach Pages through a release. Progress reports still come from `main`. For local development, build `relinker_web` with Emscripten and run `python3 tools/test_web.py --build build-wasm`. To stage a complete published release without compiling, run `python3 tools/build_web.py --build build-wasm --release`; `--libraries` is an alias for this mode. + ## Commands ```sh @@ -113,6 +121,16 @@ 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` and `LibAtrac9` always stay bundled and still need `git submodule update --init`. +## 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`. + +```sh +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. + ## Write watch tests The macOS write watch can be built and tested separately, without submodules or the prx libraries: @@ -184,7 +202,7 @@ Set `APS5_TRACE_EVENTS=` to change the queue-worker and presenter buffers Inside the window the queue workers and the presenter record every `PerformanceTimer` scope and stage as a complete event named after its frame timing key: `Driver.WorkerWait`, `Driver.Packet`, `Driver.Packet.draw`, `Graphics.Draw`, `Driver.Present`, `Driver.Present.inflight_wait`, `Vulkan.Retire` and so on. A slice shorter than 20 us is only counted: its time is in the thread's counter ` folded` (milliseconds per present) and in the summary. A thread writes a present's counter sample at its first slice after the next present, so the last present of the window has no sample; its time is still in the summary. By default, each queue worker and presenter allocates a buffer for 262,144 events when it starts; a full buffer stops that thread's recording, sets `trace_dropped` and puts an instant event `trace buffer full` on the thread's track at the start of the first lost slice (slices are appended when their outer scope ends, so that can precede the last recorded slice by up to one outer-scope length), with the number of lost slices in `args.events_lost`. When the window closes, the presenter formats the trace once and a `ProfileOutput` writer thread writes `aps5-trace--.json.gz` (Chrome trace-event JSON, opened by ui.perfetto.dev or chrome://tracing) and appends one line to `aps5-summary.jsonl`, both in `APS5_TRACE_DIR` or the working directory. A window that is still open when the title exits writes nothing. -The summary line has `commit` and `dirty` of the build, `env` (every `APS5_*` variable; a value containing `/` or `\` is written as ``), `gpu`, `driver`, `os`, `resolution` (the video output's), `window`, `presents`, `frame_ms` (p50, p90, p99 and max of the intervals between presents), `gpu_busy_ms_per_present` (the batch timestamps behind `[present]`), `worker_busy_pct` (share of the window each queue worker spends in `Driver.Packet`, `Driver.Suspend`, `Driver.EndSubmission`, `Driver.Rewind` and `Driver.Completion`), `top_phases_ms_per_frame` (the eight largest scopes and stages, without `Driver.WorkerWait`), `trace_dropped`, `trace_dropped_threads` and `trace`. `trace_dropped` is `true` when any thread lost a slice, any thread's totals table (512 scope and stage entries) was full, or the `gpu` track was full. `trace_dropped_threads` is present only then and has one entry per such thread name (threads that share a name, such as the shader prepare threads, share one entry: `events_lost` is summed, `events_full_after_s` is the earliest, `totals_full` is true if any is): `events_full_after_s` (seconds from the first present of the window to the start of the first lost slice; absent for `gpu` and, in every summary line (windowed `APS5_TRACE_FRAMES`/`APS5_TRACE_SECONDS` and `APS5_TRACE_SUMMARY_EVERY`), whenever the first lost slice starts outside the line's span of presents, including a slice whose scope began before the window opened or one lost after the window's last present), `events_lost` (slices not recorded; absent when 0; in `APS5_TRACE_SUMMARY_EVERY` lines it counts the thread buffer's losses since it started, not only those of the line's span) and `totals_full` (`true` when the thread's totals table overflowed, which makes the summary numbers incomplete; a thread with `events_lost` only has complete totals). A trace holds timings, scope names, counts and thread names; no guest memory, paths or strings of the title. +The summary line has `commit` and `dirty` of the build, `env` (every `APS5_*` variable; a value containing `/` or `\` is written as ``), `gpu`, `driver`, `os`, `cpu` (the CPU brand string), `cpu_threads` and `memory_mib` (physical memory) of the host (all three absent on a host that is not x86 or reports no brand string), `title_id`, `title` and `content_version` from `app0/sce_sys/param.json` (only when the title has one; `content_version` is empty when the file has no `contentVersion`, or one that is not a string of at most 15 characters), `resolution` (the video output's), `window`, `presents`, `frame_ms` (p50, p90, p99 and max of the intervals between presents), `gpu_busy_ms_per_present` (the batch timestamps behind `[present]`), `worker_busy_pct` (share of the window each queue worker spends in `Driver.Packet`, `Driver.Suspend`, `Driver.EndSubmission`, `Driver.Rewind` and `Driver.Completion`), `top_phases_ms_per_frame` (the eight largest scopes and stages, without `Driver.WorkerWait`), `trace_dropped`, `trace_dropped_threads` and `trace`. `trace_dropped` is `true` when any thread lost a slice, any thread's totals table (512 scope and stage entries) was full, or the `gpu` track was full. `trace_dropped_threads` is present only then and has one entry per such thread name (threads that share a name, such as the shader prepare threads, share one entry: `events_lost` is summed, `events_full_after_s` is the earliest, `totals_full` is true if any is): `events_full_after_s` (seconds from the first present of the window to the start of the first lost slice; absent for `gpu` and, in every summary line (windowed `APS5_TRACE_FRAMES`/`APS5_TRACE_SECONDS` and `APS5_TRACE_SUMMARY_EVERY`), whenever the first lost slice starts outside the line's span of presents, including a slice whose scope began before the window opened or one lost after the window's last present), `events_lost` (slices not recorded; absent when 0; in `APS5_TRACE_SUMMARY_EVERY` lines it counts the thread buffer's losses since it started, not only those of the line's span) and `totals_full` (`true` when the thread's totals table overflowed, which makes the summary numbers incomplete; a thread with `events_lost` only has complete totals). A trace holds timings, scope names, counts and thread names; no guest memory, paths or strings of the title. The shader preparation threads are traced as well, `shader prepare`, with a buffer for 32,768 events each. A wait for the GPU mutex on a traced thread is a `GpuMutex.wait.` slice (`dispatch`, `draw`, `present`, `flush` and so on, as on the `[lock]` line). @@ -200,6 +218,8 @@ Set `APS5_TRACE_SUMMARY_EVERY=` (a whole number above 0; a malformed va The switch combines with `APS5_TRACE_FRAMES` or `APS5_TRACE_SECONDS`. The intervals run for the whole session and the window records its full trace on top: the event buffers are allocated and filled only while the window is open, the window's own summary line (with `trace` set and no `interval_*` fields) holds only the time inside the window, and the interval lines continue unchanged around it. Without a window switch no buffer is allocated at all. The two switches do not share a clock origin: `interval_*` is relative to the first present, as the window's `APS5_TRACE_SECONDS` start is. +Set `APS5_TRACE_BENCH=1` together with `APS5_TRACE_SUMMARY_EVERY` to also keep every interval's frame times for a benchmark report. At each interval the presenter hands the kept intervals to a writer thread, which formats and compresses the file and rewrites `aps5-bench--.json.gz` in `APS5_TRACE_DIR` or the working directory: one JSON object with `format` (`aps5-bench/1`), the identity fields of the summary line (`commit`, `dirty`, `env`, `gpu`, `driver`, `os`, `cpu`, `cpu_threads`, `memory_mib` and, with `param.json`, `title_id`, `title` and `content_version`), `intervals_dropped` and `intervals`. Each interval has `start_s`, `end_s`, `first_present`, `resolution`, `gpu_busy_ms` (the interval's total) and `frame_us`, the whole microseconds between consecutive presents. The file keeps the last 240 intervals; `intervals_dropped` counts older ones it no longer holds. Any other value, or the switch without `APS5_TRACE_SUMMARY_EVERY`, fails the first present. The file is written to `.tmp` and then moved over the previous one, so a crash or kill during a rewrite leaves the last complete report. Only the newest report waits for the writer: one handed over while an older one is still pending replaces it. The frame times cost 4 bytes per present in memory. + `python tools/trace_digest.py aps5-summary.jsonl` prints the interval lines of such a file as one row per interval (frame p50, p90, p99, max, GPU busy per present, the busiest queue worker and the largest phase) and a text sparkline of the frame p99 and of the GPU busy time; lines of window captures in the file are skipped, and a change of commit, environment, GPU, driver or OS between intervals is reported. `--json` prints the rows as JSON. With `APS5_TRACE_SUMMARY_EVERY` set, the recorder writes a timestamp pair per batch for the whole run and every `PerformanceTimer` scope adds its time to the thread's totals, two clock reads per scope. A thread's totals are read while it runs, so a scope that straddles an interval boundary is counted in one interval or the other. diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index a309403888..bb2a757c7f 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -73,6 +73,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `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 | | `ANYPS5_REQUIRE_ROBUST2` | presence | `core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp` | | fails the LDS lock-budget test when the device does not enable robustBufferAccess2, instead of only printing it | | `ANYPS5_REQUIRE_VULKAN` | presence | `core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp` | | fails a test instead of skipping when no usable Vulkan device is present | +| `ANYPS5_SCREENSHOTS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp` | | directory the screenshot key saves its PNG files to, defaulting to anyps5-screenshots beside the executable | | `APS5_ADDRESS_COPY_MAX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 64 | upper bound in MiB on the guest memory one address-based build copies; exceeding it aborts | | `APS5_ALL_BUFFERS_WRITTEN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | notes every shader buffer element as written, as before bufferWritten existed | | `APS5_ASSERT_GPU_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | aborts when a guarded path is reached without holding GuestMemory::GpuMutex | @@ -102,6 +103,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_COUNT_ALL_COMPLETION_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | counts a completion label also stored on the GPU as pending from its registration | | `APS5_CPU_COPIES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | copies misaligned descriptor sub-ranges on the CPU instead of with a GPU buffer copy | | `APS5_CPU_DCC_KEYS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp` | | always stores DCC keys on the CPU | +| `APS5_CPU_FLIP_IN_ORDER` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp` | | orders every CPU flip behind queued work instead of presenting unqueued flips at once | | `APS5_CPU_STORES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | writes packet stores on the CPU instead of recording them on the GPU | | `APS5_DCC_KEYS_EACH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every DCC key store at once instead of batching them | | `APS5_DEPTH_COPY_MIN_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 1024 | minimum surface size in KiB for a depth target to take the depth-copy path | @@ -225,7 +227,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_FLUSH_PRETEST` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | lists every pending image on FlushPending instead of testing whether a pending unit lies inside the range | | `APS5_NO_FOREIGN_WRITERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | records no draw-written ranges as foreign writers for capture validation | | `APS5_NO_GATE_RETRY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | retries no validation whose entry moved, leaving the dispatch or draw unvalidated | -| `APS5_NO_GPL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables graphics pipeline libraries | +| `APS5_NO_GPL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | draws without graphics pipeline libraries; the device still enables their extensions, so the driver builds its pipelines as for a default device | | `APS5_NO_GPU_DUMP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | decodes dump frames from the tiled guest buffer on the CPU | | `APS5_NO_GPU_INDIRECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp` | | reads indirect dispatch arguments on the CPU instead of letting the GPU count them | | `APS5_NO_GPU_INDIRECT_DRAW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | reads indirect draw arguments through the flush hook on the CPU instead of from the GPU | @@ -379,6 +381,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_THREAD_RT` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | 10 | realtime priority the raised threads ask rtkit for (clamped to 1..99) | | `APS5_THREAD_RTTIME_US` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | 200000 | hard RLIMIT_RTTIME the realtime plan lowers to, in microseconds | | `APS5_TRACE_BARRIERS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | prints that many simulated hazard barriers before stopping | +| `APS5_TRACE_BENCH` | string | `core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp` | | `1` with `APS5_TRACE_SUMMARY_EVERY` also rewrites `aps5-bench--.json.gz` with every interval's frame times; any other value fails the first present | | `APS5_TRACE_BIGBUF` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | lists every copied guest region of at least 1 MiB | | `APS5_TRACE_CAPSYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | prints every unlocked hook wait | | `APS5_TRACE_CONTEXT_STATE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp` | | logs context-state operations and the context registers written | diff --git a/docs/dev/PROGRESS.md b/docs/dev/PROGRESS.md index 7602f54f0f..a1257f85a9 100644 --- a/docs/dev/PROGRESS.md +++ b/docs/dev/PROGRESS.md @@ -14,7 +14,7 @@ A library with no scanned definitions and no shared sources, such as one whose ` ## History -[`tools/progress_history.py`](../../tools/progress_history.py) measures the first-parent history of `main` with the same rules as `progress.py` and writes `history.json`, `history.svg` and `history.html` (the chart with a tooltip per day plus a table, linked from the README and the end of the progress page). The chart plots counts, not percentages, so it shows how much is added over time: one panel for library functions and one for shader instructions, each on its own scale with the implemented count as a solid line and the declared total as a dashed one (for shaders the declared total is the instruction list in `tools/rdna_isa.txt`); the current percentage sits next to each panel's implemented label. It takes one point per day, at the last commit of the day, and skips commits whose tree lacks the scanned paths. The chart compacts as days accumulate: up to 14 days it labels every day, up to 150 days one label per Monday, beyond that one per month; points are drawn up to 45 days, and every day keeps a hover tooltip, while the table and `history.json` keep every day. Old trees are measured with today's counting rules and instruction list, so the series is comparable but not what the page showed on that day. The workflow checks out the full history for it. +[`tools/progress_history.py`](../../tools/progress_history.py) measures the first-parent history of `main` with the same rules as `progress.py` and writes `history.json`, `history.svg` and `history.html` (the chart with a tooltip per day plus a table, linked from the README and the end of the progress page). The chart plots counts, not percentages, so it shows how much is added over time: one panel for library functions and one for shader instructions, each on its own scale with the implemented count as a solid line and the declared total as a dashed one (for shaders the declared total is the instruction list in `tools/rdna_isa.txt`); the current percentage sits next to each panel's implemented label. It takes one point per UTC day, at the last commit of the day, and skips commits whose tree lacks the scanned paths. The chart compacts as days accumulate: up to 14 days it labels every day, up to 150 days one label per Monday, beyond that one per month; points are drawn up to 45 days, and every day keeps a hover tooltip, while the table and `history.json` keep every day. Old trees are measured with today's counting rules and instruction list, so the series is comparable but not what the page showed on that day. The workflow checks out the full history for it. ## Comparisons diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 86b594ae4e..6700d98a25 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -24,6 +24,6 @@ Throughout the project, every function at every stage either **does exactly what ### Functional -- Controller haptic PCM drives Xbox-style motor rumble from its per-channel RMS (large/small motor, scale 2x, below 0.01 ignored, 80 ms hold); the mapping approximates waveform energy and is not measured against console hardware +- Controller haptic PCM that no controller sound card plays (a DualSense over Bluetooth, or any other controller) drives motor rumble from its per-channel RMS (large/small motor, scale 2x, below 0.01 ignored, 80 ms hold); the mapping approximates waveform energy and is not measured against console hardware - [Cancellation coverage](ARCHITECTURE.md#thread-cancellation) checks deferred request handling and recovery, not full asynchronous support or console ABI compatibility. Cancellation during file/socket I/O, sleep interruption, cancellation versus exit/detach, condition signal conservation under cancellation, and safe rejection of non-null invalid or reclaimed thread handles remain unverified. No console title or homebrew establishes the cleanup NIDs or buffer layout. Accepted asynchronous mode currently has no diagnostic for its deferred behavior. - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. diff --git a/docs/dev/debt/libSceAcm.md b/docs/dev/debt/libSceAcm.md index c5c9b90f30..dea77356bd 100644 --- a/docs/dev/debt/libSceAcm.md +++ b/docs/dev/debt/libSceAcm.md @@ -6,5 +6,6 @@ ### Unknown function info +- Context validation error codes (0x81940002 for bad context and 0x81940006 for bad pointer) for [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) follow prosper's `hle_audio_spatial.cpp`. KytyPS5 (`acm.cpp`) and prosper do not track individual batch identifiers, so `sceAcmBatchWait` returns success for valid contexts without inspecting batch IDs. Hardware ACM DSP coprocessor scheduling and console error verification remain unmeasured. +- [sceAcm_ConvReverb_SharedIr, sceAcm_IFFT](../../../core/libs/prx/libSceAcm/Export.cpp) (libSceAcm) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAcm.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim - PPSA02530 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceAcmBatchJobPriority](../../../core/libs/prx/libSceAcm/Export.cpp) (libSceAcm) and [scePadVrControllerGetTriggerEffectStates](../../../core/libs/prx/libScePad/Export.cpp) (libScePad). - diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 2cf456bc83..6ec4e45f5c 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -56,13 +56,14 @@ - [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. - [Texture cache budgets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - the storage image cache's budget is what the reported heap budget leaves after the heap's use other than the cached storage images, the unused part of the sampled textures' 2 GiB floor and an eighth of the budget, capped at a quarter of the budget and at `TextureCacheBudget` (a quarter of the heap's size, at least 2 GiB), never below 512 MiB; the sampled cache's budget reserves `TextureCacheBudget` for the storage cache. So the sampled budget falls first while the sampled textures alive stay within their floor, and both fall together above it; each cache trims only at its next insert, a storage image is allocated before its insert reads the budget, and both budgets read the heap at most once a second, so a burst between two reads can still exceed the reported budget. The 2 GiB floor and the eighth are the sampled formula's values reused and the 512 MiB floor and the caps the earlier ones; none was measured beyond one title (PPSA11386) on one 10 GB GPU, and a title whose storage working set exceeds the shrunk budget re-creates storage images at each use. +- [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. - [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. - [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. - [Type-0 and type-1 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 or 1 in dword 3) with a nonzero base are bound as null images, like a zero base. TYPE 0 comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault). TYPE 1 comes from a gfx1010 (RDNA1) measurement in #4139 and #5828 (`image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` return zero, no fault) with format 22 only; TYPE 1 is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). Neither type was measured on the console's gfx1013. - [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. The next target, sampled or storage lookup removes retired cache owners; recorded work and sampled views keep their images alive until their last reference is released. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. -- [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` supports at most sixteen values proved from constant, select and phi exports, with an export proved on every kept path. 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, 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; 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. +- [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, 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; 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, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, 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. @@ -95,7 +96,6 @@ - [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. Explicit-size file writes (`write`, `pwrite`, `writev`, `pwritev`, AIO and `fwrite`) synchronize their source ranges through the [host-read hook](../../../core/libs/prx/libc/src/GuestArena.cpp); other host readers are not covered. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. - [Pending storage-image results 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. -- [Storage images with DCC metadata and more than one mip](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are tracked as one unit, not as 64 KiB blocks (`blockUnits`). Tracked as blocks, the 1920x1080 six-mip DCC surfaces of PPSA28329's main menu lose their results: the menu background is missing or shows stale tiles. Storing such a surface whole at every write-back also avoids it, at a lower frame rate. Which step of the block path loses the results is not identified. - 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) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until a GPU use of the import completes; a 4-byte read of it on the settle queue ends that even while main-queue work that used the import first is still running, and an earlier import of the same pages whose first use runs after that read marks none of them (`agc_import_settle_first_use`, `agc_import_settle_earlier_import`). Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists is compared instead. An import of under 1 MiB is not settled inside its window, because the settle blocks the importing thread and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). It is compared until its read, submitted in a batch by a later import lookup under `GpuMutex` while the imports match the registry, has completed; a later lookup then returns its whole blocks to the write watch, each stamped as a CPU store, so whatever was derived from them while they were compared reads as changed once. Its blocks stay compared while storage results are pending over them, and those of an import retired before that stay compared. - [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made at map time settles on the guest thread that maps the range, inside its map call, so that thread waits while the kernel maps the pages: 0.47 to 2.2 s for the 3 GiB range of Hades (PPSA03355) and 0.49 to 4.3 s for the 4.5 GiB range of ASTRO BOT (PPSA21567) on RADV. Both titles map these ranges at start-up; a title that maps large ranges during gameplay stalls its mapping thread for as long. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission. A range imported through a host pointer, as lavapipe imports shared memory, is not settled @@ -107,6 +107,7 @@ - [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. - [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. @@ -132,12 +133,12 @@ - [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 - [CMASK fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - each fast-cleared CMASK tile (code 0) is stored as `CB_COLOR_CLEAR_WORD` into its 8x8 texels at the first draw into the target or at its fast-clear eliminate or DCC decompress pass, not as the color block writes the tile, and the CMASK is then stored as expanded (0xF): a texture or copy read of the target after that draw and before the title's eliminate sees the clear value in tiles no draw wrote, where the console keeps their previous bytes. Tile addresses follow the pipe-aligned SW_64KB_Z_X CMASK equation for 16 pipes; other CMASK swizzle modes, a cleared tile of a DCC target and a fast clear of a 3D or array color target throw; base mip 0 of a 2D mipmapped color target is supported (offset follows AMD Addrlib `Gfx10Lib::HwlComputeCmaskInfo` reverse mip-chain ordering preceding base mip 0, unverified on PS5 hardware; mipmapped targets with 8 or 16 bpp throw until `dsMipmapHtileFix` mip-tail shrinkage is modelled). A tile with a code other than 0 and 0xF keeps its texels like an expanded tile: PPSA18391 (engine: Clickteam Fusion) fast clears a target whose CMASK it never initialized, so some of its nibbles hold leftover values such as 1 and 10; what the console does with such codes on a single-sample target is unverified on hardware. Single-sample views with `viewMip > 0` skip CMASK fast-clear decoding since CMASK metadata only covers base mip 0 on AMD hardware (Mesa `si_state.c` similarly clears `FAST_CLEAR` for level > 0). -- [CMASK clear pass on AMD without pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp) (libSceAgcDriver) - the compute pass that stores a fast-cleared CMASK into a resident render target (`DispatchCmaskClear`) is unverified on AMD's Windows driver with `APS5_NO_GPL=1`: on an RX 6400 (driver 32.0.21033.3005, Windows 10 19045) `agc_driver_cmask_fast_clear` loses the device in its `SW_64KB_R_X` pass (`device-fault` read invalid at 0xbeefdeadc000, then Vulkan result -4); with pipeline libraries on, and on an NVIDIA RTX 4080 SUPER in both modes, it passes. On a Radeon 860M (driver 32.0.13058.8001, Windows 11) the same pass keeps the stale 0x5A5A5A5A instead of the clear value, without a device loss. The reading shader and the reason pipeline libraries matter are not found (#5150). Closes when: the cause is found, or `ctest -R '^agc_driver_cmask_fast_clear$' --repeat until-fail:5` passes with `ANYPS5_GPU=Radeon` and `APS5_NO_GPL=1` on AMD. +- [CMASK clear pass on AMD without pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp) (libSceAgcDriver) - the compute pass that stores a fast-cleared CMASK into a resident render target (`DispatchCmaskClear`) is unverified on AMD's Windows driver with `APS5_NO_GPL=1`: on a Radeon 860M (driver 32.0.13058.8001, Windows 11) `agc_driver_cmask_fast_clear` keeps the stale 0x5A5A5A5A in its `SW_64KB_R_X` pass instead of the clear value, while it passes with pipeline libraries on and on an NVIDIA RTX 4080 SUPER in both modes. An RX 6400 (driver 32.0.21033.3005) lost the device (read invalid at 0xbeefdeadc000) in an earlier build tree, but clean rebuilds of four main commits of 19 October pass there (three of three runs each), so that failure is not reproduced. The cause is not found (#5150). Closes when: the cause is found, or `ctest -R '^agc_driver_cmask_fast_clear$' --repeat until-fail:5` passes with `ANYPS5_GPU=Radeon` and `APS5_NO_GPL=1` on AMD. - [HTILE depth fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. A second kernel, the 9-word dword fill PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE with (64x1x1 groups, one `buffer_store_format_x` per thread through the stride-4 V# in `user[0..3]`, the value read by `s_buffer_load_dword` through the V# in `user[4..7]`), is taken as a fill of that range when the store format is 32_UINT, 32_SINT or 32_FLOAT and the value V# covers the dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes when the stride is 0) ([`MatchMemoryValueFill`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp), then the whole-HTILE and ZMask rules of `NoteDepthMetadataFill`). The value is read from guest memory when the dispatch is decoded, so a value that earlier GPU work in the same submission has not written back yet is not seen. When the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes the dispatch instead (an output descriptor of one 32-bit value per element, a value descriptor that covers the loaded dword, a 16-byte aligned range), the value is read again when the fill is recorded, after the labels queued before it, and the HTILE fill is noted with that value. - [HTILE written by other kernels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - when a depth target is bound and its HTILE was written since it was last read, the words over its whole pipe-aligned metadata (`HtileMetadataBytes`, the extent `HtileFillCovers` takes) are read, outside the surfaces mutex since the read waits for recorded work: when every one marks the depth (ZMask 0) or the stencil (SMem 0) cleared, the surface takes that fast clear and the words are stored back as expanded (depth only: ZMask 15 over the full Z range; with stencil: ZMask 15, Z range and SMem, SR1 and SR0 3), so only the title's next clear clears it again. PPSA29141 clears its depth every frame with a compute kernel the fill hooks do not recognize (`InvokeHTileClearComputeShader` in its `ClearMRT` pass); without this its depth kept every earlier frame and moving objects left black trails. PPSA25344 (Unreal Engine 5) reads its HTILE after the depth prepass in its Nanite depth export, which takes a tile with ZMask 0 as holding the clear value; without the expanded words the export put Nanite depth over the prepass and the Nanite material pass painted over the meshes in front. The word layouts are Mesa radv's for GFX10. Metadata with some tiles cleared is not modelled, and the compression state draws leave in HTILE is not tracked, so a title reading its HTILE after a draw sees the stored expanded words. A recognized HTILE fill or clear makes the next bind read the words again. A slice other than 0 of a depth array is read at its own HTILE (`DB_HTILE_DATA_BASE` plus the slice times `HtileMetadataBytes`) but the HTILE word layout of slices 1 and up is not modelled (PPSA05684's tile masks need X8 and Y8 to take Z0 and Z1 as well, slices 4 and up unmeasured), which neither the check that every word is cleared nor the store-back of each word in place needs, so such a slice takes the clear like slice 0. A depth-only HTILE whose tiles all have ZMask 0 but different Min/Max Z (per-tile clear values, PPSA05684's cascades) is taken as a whole-surface clear to `DB_DEPTH_CLEAR`, the value the DB's depth test reads for a cleared tile (`HTILE_Z_CLEAR_REG` in Mesa's `src/amd/common/ac_descriptors.h`: the clear word "only clears HiZ", "the DB register contain[s] the full clear values"); the HiZ test before it, which rejects against each tile's own Min/Max Z range (the HTILE definitions in the same file), is not modelled, so a draw that HiZ would reject over a tile cleared with Min/Max Z 0 is tested against `DB_DEPTH_CLEAR` instead. A multisampled depth target, and any target whose HTILE overlaps a resident native depth guard, skips this read and store-back and goes to the native multisample depth owner, which keeps its own refusal for a fast-clear transition. - [RADV fill-memory kernel](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) (libSceAgcDriver) - the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes RADV's 16-byte `fill_memory` compute kernel (`MatchClampedGlobalFill`: 11 GFX10.3 words, 64x1x1 groups, six user SGPRs with the address in `user[2..3]`, the largest store offset in `user[4]` and the value in `user[5]`; each thread stores 16 bytes at `min(thread * 16, user[4])`) as a fill of `min(threads * 16, user[4] + 16)` bytes. RADV initializes and fast-clears DCC keys and other metadata with it, so the key and metadata tracking sees such a fill where it is in the command stream; run as a dispatch, its global stores name no range, and a key fill was only seen once it had landed, after later writes to the surface. Only a 16-byte aligned address and largest offset are taken. RADV's 4-byte variant (fills below 16 bytes), other Mesa versions' encodings and other compilers still run as dispatches. The kernel words are the ones vkQuake's PS5 RADV build (Mesa 25.2) submitted; they are not compared with the NIR builder's output for other targets. - [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import: the largest aligned power-of-two block, from twice the import alignment up to the 8 MiB window, that holds the requested address and that they fill exactly, so a block only grows by doubling as its window fills. A block that would exceed the budget, would replace an import a build holds, or fails (a refused Windows alias, `vkAllocateMemory`) steps down to the next smaller block; a failed block is not tried again until one of its ranges changes. Ranges larger than the window are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window, or bound exactly over adjacent ranges of any size (from the first one's start to the last one's end), joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. A heap grown in pieces re-imports its joined run only when a buffer is bound exactly over all of its pieces, or when a sparse descriptor heap over it is bound: a heap a shader binds whole while only the pieces from its base are mapped is bound in place from the import of that run, its views ending at the import's end as the copy's do, once the previous bind of that address saw the same last mapped byte (a heap that grows between every two of its binds stays copied; one bound more often than it grows is joined again after each growth) and at most 64 times per joined run base; past that, with a message, it is not joined again: it is bound in place while an existing import covers its run and copied otherwise, until no registered range starts at that base or the device changes. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. -- [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 or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. 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. +- [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. - [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. @@ -206,7 +207,11 @@ - [Thick SW_4KB_S_X volumes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) use AMD addrlib's GFX10 non-RB+ 16-pipe `GFX10_SW_4K_S3_X_PATINFO` equations for 1, 2, 4, 8 and 16-byte elements, including packed mip tails. The layout follows [AMD PAL c5e80007](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp); it has not been measured on console hardware. Other thick swizzle modes outside the supported families still throw. Mipmapped 3D color targets remain refused by the color-register decoder. - [Viewport depth range remap](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - on a device without `VK_EXT_depth_range_unrestricted` a viewport depth range outside [0, 1] (Minecraft PPSA17221 draws its world with `PA_CL_VPORT_ZSCALE` 1, `ZOFFSET` 0 in negative-one-to-one clip space) is folded into the vertex shader: `RemapPositionDepth` scales clip z so z/w is the console's depth, the pipeline's depth clamp applies the console's `PA_SC_VPORT_ZMIN/ZMAX` clipped to [0, 1] (exact for a normalized depth target or none; Minecraft clamps its float depth target to [-1, 1], so a depth below 0 is stored as 0 there, which only a later test or read at exactly 0 can tell, and the first such draw is reported once), and two extra `gl_ClipDistance` elements restore the near and far planes depth clamping removes. The mode is a pipeline specialization constant (`PipelineSpecialization::DepthRemapBase`); scale and offset are read from the draw's push block (two words at byte 104, `DepthRemapPushOffsetBytes`, so a remapped draw's stage push data ends before it), so draws that differ only in them share one vertex module and one pipeline. The remap serves the vertex path only; mesh and tessellation draws with such a range still throw. Whether the console clamps before or after its depth bias and how it treats a clamp range narrower than the transform's are not measured. +- [Benchmark rewrite failure test off Windows](../../../core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp) (libSceAgcDriver) - `ReplaceGzipFile` checks that a write failing at `.tmp` keeps the previous `aps5-bench.json.gz` only on non-Windows builds: the `std::ios_base::failure` thrown inside the MinGW ofstream did not unwind to the test's catch, so Windows CI covers only the failed move. - [Sampled array windows](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) (libSceAgcDriver) - a sampled 2D array, 1D array or cube view whose base array slice is past 0 is built from its viewed slices only (`ViewedArrayWindow`), at the guest address of its base slice. The window is taken only when the surface is thin, has no DCC metadata, and, for a 64 KiB XOR swizzle, when the base slice keeps every slice bit of the swizzle equation at zero (R_X 1-byte elements: a base slice that is a multiple of 16). Any other view still builds slices 0 to its last slice. A window and a whole-array view of the same surface are separate images over overlapping memory, kept coherent by the existing overlap rules, at the cost of the memory and uploads of both. Ghost of Yotei (PPSA26344) samples a 256x256 cube array at slices 4299..7232, built at 1467 MiB instead of about 3.8 GB. - [2D reads of 2D arrays](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled image whose shader shape is 2D and whose T# is a 2D array is built from its base slice alone: the T# is narrowed to that slice (`FirstLayerDescriptor`) before its decode, cache key and image, in stage A records, stage B bindings, revalidation and image tables. A whole-array read of the same T# keeps its own cache entry, so both images exist when a title uses both. Storage images read or written through a 2D shape are not narrowed and still build every slice the T# names. Ghost of Yotei (PPSA26344) reads a 3328x1872 R_X T# of 1537 slices this way: the whole array was a 12.4 GiB image that did not fit the device-local budget of an RTX 4090. - [Sampled and storage views of 1 GiB or more](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) that stay whole are only logged (sampled views by `cachedTexture`, storage views by `cachedStorageTexture`), with the registered guest range holding their base and how many of their slices fit in it; nothing is capped to that range. Ghost of Yotei (PPSA26344) samples a 1467 MiB cube array window of which only 838 of 2934 slices lie inside its 3380 MiB range: the rest is read from whatever follows the range. Whether such slices are ever sampled is not measured. +- [Color export type against the target format](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - the export-format check refuses a `UINT16_ABGR` export (`SPI_SHADER_COL_FORMAT` 7) into a target whose color-buffer number format is not unsigned integer, but not the reverse. A 32-bit export (`32_R`, `32_GR`, `32_AR` or `32_ABGR`) into an unsigned-integer target leaves `uintExport` clear, so the recompiler declares the fragment output `float4`; `DecodeColorBuffer` maps a number format of 4 to `R32_UINT` from the color buffer alone, independent of the export format, so the float output reaches an integer attachment. Desktop drivers keep the raw bits, but the Khronos validation layer reports `Undefined-Value-ShaderFragmentOutputMismatch` and MoltenVK's Metal compiler refuses the pipeline. Which export format a title pairs with a 32-bit integer target is not measured, so the output type is left following the export format rather than the attachment. + +- [Long Windows pipeline-cache paths](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp): `readHeader` and the failed-read existence check still use ordinary paths. A filename beyond MAX_PATH can prevent header reuse or be reported missing; the shared content reader and journal normalization do not cover these calls. diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index ca2298edef..4f972627f2 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -2,6 +2,8 @@ ### Unknown function info +- [sceAjmBatchGetLog, sceAjmInstanceAvailable](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjm.native) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAjm.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. +- [sceAjmInstanceCodecType, sceAjmInstanceExtend, sceAjmInstanceSwitch](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjm.native) - their signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ajm/ajm.h#L19-L20) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ajm.cs#L100-L104), not measured on a console; these throwing declarations only resolve imports. - [sceAjmInstanceCreate](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a full 14-bit instance slot namespace returns `0x80930007` (`OUT_OF_RESOURCES`), as [shadPS4's InstanceCreate](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ajm/ajm_context.cpp) does for its own instance pool (PS4). The 16384 slots per codec prefix are this runtime's existing ID layout, not a measured console capacity; the PS5 capacity and exhaustion result are unverified. - [sceAjmDecWVorbisCreateHeaderPacket, sceAjmDecWVorbisCreateSetupPacket](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjmWVorbis, implemented in libSceAjm.native) - unknown signatures - [sceAjmBatchJobGetInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the format reports bitrate 0 for ATRAC9 as in KytyPS5, while [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it from the superframe size; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder @@ -18,6 +20,8 @@ ### Functional +- [AJM batch contexts](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): start, wait and cancel reject context IDs not allocated by initialization; batches belong to their creating context, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0ebea15c24253b60f514fc37a348dabc8d0bcb90/src/core/libraries/ajm/ajm.cpp). This is not verified on PS5. Finalization still does not invalidate contexts or release their instances and outstanding batches; jobs execute synchronously and cancellation does not interrupt them. + - [sceAjmDecMp3ParseFrame](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) throws when the original file length is requested (`parse_ofl` non-zero) for a layer other than III, for a CRC-protected frame, or for a VBRI header outside MPEG-1 stereo: shadPS4 looks for the tag after the CRC and after the side information, while LAME and FFmpeg place it without the CRC and VBRI always at byte 36. - [ATRAC9 gapless segments](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a run job stops after the gapless total of output samples counted from the start of the segment, leaving the rest of its input unconsumed, and the next run starts the next segment with the decoder as it is and without a second skip. FMOD in PPSA29141 sets a total shorter than a sound's frames and reads the frames after it on later runs; with the output held at the total its stream reader waited for them forever and the last buffer repeated. The console's handling of the frames after the total is not documented; a reset (`SetGaplessDecode` with the reset flag, `ClearContext`, initialization) starts a new segment. An ATRAC9 run job without `RUN_MULTIPLE_FRAMES` decodes its whole input as one with it: FMOD in PPSA29141 puts two run jobs in one batch with the second's input right after the whole of the first's. Whether the console stops after one frame there is not verified; MP3 and Opus run jobs still stop after one frame - [AJM M4AAC decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 2) - Ghost of Yotei (PPSA26344) initializes it through `sceAjmBatchJobControl`. The initialize parameters (`u32` config type, 1 ADTS or 2 raw, then `u32` sampling frequency index), instance flags (channels in bits 3-6, `ENABLE_SBR_DECODE` bit 32, `ENABLE_NONDELAY_OUTPUT` bit 33, surround interleave order bits 34-35), `{heaac, reserved}` codec info and decoder error result (`CODEC_ERROR | FATAL`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/ajm/ajm_aac.cpp) (PS4, fdk-aac) and are unverified on PS5; decoding uses FFmpeg's AAC decoder. Without `ENABLE_NONDELAY_OUTPUT`, the first two frames after initialization or reset are consumed without output. Raw input takes its channel configuration from the instance (0 means stereo) and is decoded as one access unit per run job. The format reports the last frame's bitrate. Invalid config type or raw sampling frequency index above 11 returns invalid-parameter result 4. The implementation throws for ADTS without a frame header, profiles other than AAC LC, multiple raw data blocks per ADTS frame, unsupported channel configurations, more channels than the instance supports, HE-AAC without `ENABLE_SBR_DECODE`, raw input with `RUN_MULTIPLE_FRAMES`, output room smaller than the previous frame length, and unknown codec flags. diff --git a/docs/dev/debt/libSceAppChecker.md b/docs/dev/debt/libSceAppChecker.md new file mode 100644 index 0000000000..9fc80d43e0 --- /dev/null +++ b/docs/dev/debt/libSceAppChecker.md @@ -0,0 +1,5 @@ +# libSceAppChecker technical debt + +### Unknown function info + +- [sceAppCheckerAbort, sceAppCheckerExecute and sceAppCheckerExecuteEx](../../../core/libs/prx/libSceAppChecker/Export.cpp) (libSceAppChecker) - names and NIDs are present in the [aerolib symbol database](https://github.com/zecoxao/sce_symbols/blob/master/aerolib.csv) and are attributed to this module by `sceAppChecker` symbol prefix; the library is absent from the PS5 3.20 export inventory, so the pairing is not firmware-verified and no credible prototypes were found. The declarations have no parameters solely to resolve imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 5639476d66..bf943bde0e 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -3,7 +3,7 @@ ### Unknown function info - [sceAppContentDownloadDataGetAvailableSpaceKb](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - a title whose `param.json` declares `downloadDataSize` 0 gets 0 KB and OK; the console's behaviour for a title without a download data area is unverified (it may return an error instead). KytyPS5 does not special-case it, and shadPS4 stubs 1048576 KB -- [sceAppContentDownloadDataFormat](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - empties the `/download0` directory, creating it if needed. Imported by PPSA01317. A null mount point returns `PARAMETER` as `sceAppContentTemporaryDataFormat` does, and another mount point throws as in `sceAppContentDownloadDataGetAvailableSpaceKb`; the console's error codes for both and its behaviour with files still open are unverified +- [sceAppContentDownloadDataFormat](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - empties the `/download0` directory, creating it if needed. Imported by PPSA01317. A null mount point returns `PARAMETER` and another mount point returns `NOT_MOUNTED` as in `sceAppContentTemporaryDataFormat` and `sceAppContentDownloadDataGetAvailableSpaceKb`; the console's error codes for both and its behaviour with files still open are unverified ### Functional diff --git a/docs/dev/debt/libSceAudioIn.md b/docs/dev/debt/libSceAudioIn.md index 99e0629243..63b2d8b02c 100644 --- a/docs/dev/debt/libSceAudioIn.md +++ b/docs/dev/debt/libSceAudioIn.md @@ -11,3 +11,4 @@ ### Functional - [sceAudioInHqOpen, sceAudioInAsyncOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) open the same port as `sceAudioInOpen`, so `sceAudioInInput`, `sceAudioInGetSilentState` and `sceAudioInClose` accept their handles. The signatures agree across [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/fc5d2cc219c43c8c8f354153e7b3d0c04a5d506e/src/core/libraries/audio/audioin.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp). HqOpen is limited to 128 samples at 48 kHz, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) describe the higher-quality path: a longer grain returns the invalid-size error and a lower rate the invalid-frequency error. AsyncOpen also accepts the 384-sample grain, the largest [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) documents for the low-latency path, and returns the invalid-size error for every other size; the console's shorter average return interval of an async port's blocks is not modelled, so like a synchronous port `sceAudioInInput` paces one grain at a time. Param 0x10 opens a float mono port like 0x11, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp) accepts both, while SharpProspero documents only 0x11. Host capture pacing and console behavior remain unverified. +- [sceAudioInGetGain, sceAudioInGetRerouteCount](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - signatures and semantics follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs#L92-L110). `sceAudioInGetGain` writes the unattenuated linear gain `1.0f` into `*gain` before validating `handle`, returning `0` for an open port and `0x80260101` (`SCE_AUDIO_IN_ERROR_INVALID_HANDLE`) otherwise; a null `gain` pointer throws because the console result is unverified, and host or system input attenuation is not modelled. `sceAudioInGetRerouteCount` returns `0` for an open port and `0x80260101` otherwise, as each port opens its SDL capture device once and capture-device hot-swapping is not modelled. diff --git a/docs/dev/debt/libSceAudioLatencyEstimation.md b/docs/dev/debt/libSceAudioLatencyEstimation.md new file mode 100644 index 0000000000..9e199bf5f3 --- /dev/null +++ b/docs/dev/debt/libSceAudioLatencyEstimation.md @@ -0,0 +1,5 @@ +# libSceAudioLatencyEstimation technical debt + +### Unknown function info + +- [sceAudioLatencyEstimationAddData, sceAudioLatencyEstimationGetDelayTime, sceAudioLatencyEstimationInitialize and sceAudioLatencyEstimationRelease](../../../core/libs/prx/libSceAudioLatencyEstimation/Export.cpp) (libSceAudioLatencyEstimation) - names and NIDs are present in the [aerolib symbol database](https://github.com/zecoxao/sce_symbols/blob/master/aerolib.csv) and are attributed to this module by `sceAudioLatencyEstimation` symbol prefix; the library is absent from the PS5 3.20 export inventory, so the pairing is not firmware-verified and no credible prototypes were found. The declarations have no parameters solely to resolve imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 43372d1a15..905733011a 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -24,6 +24,102 @@ - [sceAudioOutDeviceIdOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA01317, which fails to load without it; declared without parameters, signature unknown. It opens a port on a given audio device; which device ids exist and how they map to host outputs is unknown, so it throws - [sceAudioOut2PortGetState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) and [sceAudioOut2GetSpeakerInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.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. 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). 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 returns `0x80268006` before `sceAudioOut2Initialize`, `0x80268001` for flags above 1 and `0x8026800C` for a null pointer, which are not modelled. 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 +- [sceAudioOut2UserCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortDestroy](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextDestroy](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutOpenEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortSetAttributes](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextAdvance](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextPush](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextGetQueueLevel](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortGetState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetHandleStatusInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetSimulatedHandleStatusInfo2](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutChangeAppModuleState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetSimulatedHandleStatusInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetSimulatedBusUsableStatusByBusType](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutAttachToApplicationByPid](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutDetachFromApplicationByPid](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutPtOpen](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutPtClose](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutPtGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExPtOpen](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExPtClose](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExPtGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetInfoOpenNum](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutMasteringInit](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutMasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutMasteringGetState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutMasteringTerm](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetPadSpkVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetPadJackVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetUsbVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [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 +- [sceAudioOutSetAllMute](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetSparkEQ](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutArbitrationInitialize](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2ArbitrationInitialize](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnable3DAudioByArbitration](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutAssignHRTF](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnable3DAudioForUser](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableMonoHpOutput](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableTVVirtual](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableTVCorrection](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableVrControllerLowPowerMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableCinematicVrRotation](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutCustomHrtfIsExist](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExConfigureOutputMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExSystemInfoIsSupportedAudioOutExMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExGetSystemInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExGetOutputInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysConfigureOutputMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysGetMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysHdmiMonitorInfoIsSupportedAudioOutMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysGetSystemInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysGetOutputInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextQueryMemory](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextCreate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextSetAttributes](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusInit](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetPortConnections](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetConnections](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetPortStatuses](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetHeadphoneOutMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusUserLoginFor3DAudio](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetDeviceConnections](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetMorpheusParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetCaesarParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetSparkParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2AllAudioMainToHeadphoneForDiag](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortCreate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortGetStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2ReportRegisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2ReportUnregisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetTvCorrectionInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetIpcHandle](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetSystemStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MasteringInitEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2SetDebugHeadphoneMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2DebugStateCtrl](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoInit](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2ContextBedWrite](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2PortCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2InitializeEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [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 +- [sceAudioOut2GetAmbisonicsEncodeCoefficients](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetHrtfIdForCronos](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature ### Functional diff --git a/docs/dev/debt/libSceAutoMounterClient.md b/docs/dev/debt/libSceAutoMounterClient.md index 10f8710cf9..b7200004e3 100644 --- a/docs/dev/debt/libSceAutoMounterClient.md +++ b/docs/dev/debt/libSceAutoMounterClient.md @@ -2,4 +2,4 @@ ### Unknown function info -- [libSceAutoMounterClient](../../../core/libs/prx/libSceAutoMounterClient/Export.cpp) - the three names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAutoMounterClient.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and the exports must not be called until their signatures are verified +- [sceAutoMounterClientRegisterCallback, sceAutoMounterClientTerm and sceAutoMounterClientUnregisterCallback](../../../core/libs/prx/libSceAutoMounterClient/Export.cpp) (libSceAutoMounterClient) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAutoMounterClient.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. diff --git a/docs/dev/debt/libSceBluetoothHid.md b/docs/dev/debt/libSceBluetoothHid.md index d5e47f67fa..b8ec7bf2ab 100644 --- a/docs/dev/debt/libSceBluetoothHid.md +++ b/docs/dev/debt/libSceBluetoothHid.md @@ -7,3 +7,4 @@ ### Unknown function info - [libSceBluetoothHid](../../../core/libs/prx/libSceBluetoothHid/Export.cpp) (libSceBluetoothHid) - signatures taken from PPSA01317: `sceBluetoothHidInit()` and `sceBluetoothHidUnregisterCallback()` without arguments, `sceBluetoothHidRegisterCallback(callback, 0, param)` with a 16-byte param, and `sceBluetoothHidRegisterDevice` / `sceBluetoothHidUnregisterDevice(vendorId, productId)`; the report and device name exports are declared without parameters +- [sceBluetoothHidDebugGetVersion, sceBluetoothHidDisconnectDevice, sceBluetoothHidGetDeviceInfo, sceBluetoothHidGetFeatureReport, sceBluetoothHidGetReportDescriptor, sceBluetoothHidInterruptOutput, sceBluetoothHidParamInitialize, sceBluetoothHidSetFeatureReport, sceBluetoothHidThreadParamInitialize](../../../core/libs/prx/libSceBluetoothHid/Export.cpp) (libSceBluetoothHid) - throwing stubs declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index f517c2c7af..996e17192a 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -8,4 +8,6 @@ ### Unknown function info +- [Unicode scalar conversions](../../../core/libs/prx/libSceCesCs/Unicode.cpp) (libSceCesCs) - console error precedence, optional length pointers and length units are unverified. Signatures follow the [Orbital type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); names are in the aerolib table pinned by `tools/nid_names.py`. Encoding follows [Unicode section 3.9](https://www.unicode.org/versions/Unicode17.0.0/core-spec/chapter-3/). Each call converts one scalar, including zero and U+FEFF, without a terminator or byte-order detection; lengths count bytes for UTF-8 and 16-bit units for UTF-16. Native forms use host byte order (supported hosts are little-endian). Errors follow the existing CES converters, not console measurements, and failure leaves outputs unchanged. No console or title comparison has been run +- [sceCesEucJpToUtf8](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the scalar conversion's errors and optional length pointers are unverified against the console. Its signature follows the Orbital type database linked above; it uses the existing CP51932 profile and JIS tables, with their mapping and profile-layout assumptions listed above, converts one character (including zero), and writes no terminator. Failure leaves the output and length pointers unchanged - [sceCesRefersUcsProfileCp1250](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with the Windows-1250 table of Microsoft's [bestfit1250.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1250.txt), where the unassigned bytes map to their Latin-1 control characters as the Cp1252 and Cp1251 tables do; the console behavior for those bytes and the whole profile is not verified. PPSA34501 fails to start without this export (`qYu6y1E0Qbg`) diff --git a/docs/dev/debt/libSceCompanionHttpd.md b/docs/dev/debt/libSceCompanionHttpd.md new file mode 100644 index 0000000000..2738a6bd6d --- /dev/null +++ b/docs/dev/debt/libSceCompanionHttpd.md @@ -0,0 +1,5 @@ +# libSceCompanionHttpd technical debt + +### Unknown function info + +- [sceCompanionHttpdAddHeader, sceCompanionHttpdGet2ndScreenStatus, sceCompanionHttpdGetEvent, sceCompanionHttpdGetUserId, sceCompanionHttpdInitialize, sceCompanionHttpdInitialize2, sceCompanionHttpdOptParamInitialize, sceCompanionHttpdRegisterRequestBodyReceptionCallback, sceCompanionHttpdRegisterRequestCallback, sceCompanionHttpdRegisterRequestCallback2, sceCompanionHttpdSetBody, sceCompanionHttpdSetStatus, sceCompanionHttpdStart, sceCompanionHttpdStop, sceCompanionHttpdTerminate, sceCompanionHttpdUnregisterRequestBodyReceptionCallback and sceCompanionHttpdUnregisterRequestCallback](../../../core/libs/prx/libSceCompanionHttpd/Export.cpp) (libSceCompanionHttpd) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCompanionHttpd.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceCompanionUtil.md b/docs/dev/debt/libSceCompanionUtil.md new file mode 100644 index 0000000000..48d3c1cb47 --- /dev/null +++ b/docs/dev/debt/libSceCompanionUtil.md @@ -0,0 +1,5 @@ +# libSceCompanionUtil technical debt + +### Unknown function info + +- [sceCompanionUtilGetEvent, sceCompanionUtilGetRemoteOskEvent, sceCompanionUtilInitialize, sceCompanionUtilOptParamInitialize and sceCompanionUtilTerminate](../../../core/libs/prx/libSceCompanionUtil/Export.cpp) (libSceCompanionUtil) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCompanionUtil.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceContentExport.md b/docs/dev/debt/libSceContentExport.md index 1c8c0c1285..18355bf87b 100644 --- a/docs/dev/debt/libSceContentExport.md +++ b/docs/dev/debt/libSceContentExport.md @@ -3,3 +3,8 @@ ### Functional - [Content export sessions](../../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport): `sceContentExportStart` hands out positive session ids, at most 10 live, which `sceContentExportFinish` closes. The signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) and the limit `MaxExecution` [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentExport.cs); the parameter layouts follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/content_export/content_export.h). The host has no media library, so `sceContentExportFromDataWithThumbnail` throws once the session is checked, before calling either data callback, and no export ever runs. `sceContentExportGetProgress` and `sceContentExportCancel` throw for a live session, as their result with no running export is unknown. An eleventh `sceContentExportStart` and `sceContentExportTerm` with live sessions throw: whether the console answers `EXECUTION_MAX` or `BUSY` there, or ends the sessions, is unverified. Session id values and error precedence are unverified + +### Unknown function info + +- PlayStation 5 firmware exports declared without parameters, signatures unknown: [sceContentExportAddToFavorites, sceContentExportDataProvideFunction, sceContentExportFree, sceContentExportFrom, sceContentExportFromFileForSf, sceContentExportFromFileWithContentIdList, sceContentExportFromFileWithTitleIdList, sceContentExportInit, sceContentExportMalloc, sceContentExportRemoveFromFavorites, sceContentExportRenameContent, sceContentExportValidateContents](../../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport). Names from the firmware NID database; unimplemented and throw `NotImplemented_nid_no_patch`. + diff --git a/docs/dev/debt/libSceContentSearch.md b/docs/dev/debt/libSceContentSearch.md index 2222020f2e..56c59f62ad 100644 --- a/docs/dev/debt/libSceContentSearch.md +++ b/docs/dev/debt/libSceContentSearch.md @@ -1,5 +1,9 @@ # libSceContentSearch technical debt +### Unknown function info + +- [sceContentBinderEnd, sceContentBinderGetNumOfDifference, sceContentBinderInitialize, sceContentBinderIsUpdated, sceContentBinderReset, sceContentBinderSearchDifference, sceContentBinderStart, sceContentBinderTerminate, sceContentSearchGetApplicationLastUpdateId, sceContentSearchGetMyApplicationIndex, sceContentSearchGetNumOfApplication and sceContentSearchSearchApplication](../../../core/libs/prx/libSceContentSearch/Export.cpp) (libSceContentSearch) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceContentSearch.c), but no credible prototypes were found in the cited sources. They are declared without parameters solely to resolve known imports and make no ABI claim. + ### Functional - [libSceContentSearch](../../../core/libs/prx/libSceContentSearch/Export.cpp) searches a content library that is empty on the host, as nothing is exported to it. The signatures follow [AlexAltea/orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L555-L569) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentSearch.cs#L255-L316), which agree; the filter and sort layouts and enums, the limit of 92 rows, `0x809D100A` for a larger limit and `0x809D1001` before `sceContentSearchInit` are SharpProspero's. `sceContentSearchGetNumOfContent`, `sceContentSearchGetTotalContentSize` and `sceContentSearchSearchContent` report 0 rows and 0 bytes without writing a content row, and the update id they and `sceContentSearchGetContentLastUpdateId` report is always 0; no reference gives the console's update id for a library that never changed. Filters and sort conditions are only checked against SharpProspero's enums, and a null output, a null array with a non-zero length, an empty column set, a null filter value or an unknown enum value throws, as the console's errors for them are unknown; a null column set with length 0 is taken as no filter. `sceContentSearchOpenMetadata` and `sceContentSearchOpenMetadataByContentId` throw, as no reference gives the error for a path or content id that is not in the library, so no metadata id is ever handed out and `sceContentSearchCloseMetadata`, `sceContentSearchGetMetadataFieldInfo` and `sceContentSearchGetMetadataValue` throw. Every title is taken to hold the content-search permission (SharpProspero's `0x809D1013` is never returned). The order of the initialization, argument and limit checks is unverified diff --git a/docs/dev/debt/libSceCustomMusicCore.md b/docs/dev/debt/libSceCustomMusicCore.md new file mode 100644 index 0000000000..9ffaf08475 --- /dev/null +++ b/docs/dev/debt/libSceCustomMusicCore.md @@ -0,0 +1,5 @@ +# libSceCustomMusicCore technical debt + +### Unknown function info + +- [sceCustomMusicCoreBgmClose, sceCustomMusicCoreBgmOpen, sceCustomMusicCoreBgmOpen2, sceCustomMusicCoreBgmOutput, sceCustomMusicCoreBgmSetAudioVolume, sceCustomMusicCoreBgmStart, sceCustomMusicCoreBgmStop, sceCustomMusicCoreGetBgmAuthorityStatus, sceCustomMusicCoreGetHdcpStatus, sceCustomMusicCoreGetSystemAudioVolume, sceCustomMusicCorePostEvent, sceCustomMusicCoreSendEvent, sceCustomMusicCoreSendMulticastEvent, sceCustomMusicCoreSendSpMessage, sceCustomMusicCoreSetSystemAudioVolume, sceCustomMusicCoreStartOperation, sceCustomMusicCoreStartOperation2 and sceCustomMusicCoreStopOperation](../../../core/libs/prx/libSceCustomMusicCore/Export.cpp) (libSceCustomMusicCore) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCustomMusicCore.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceDiscId.md b/docs/dev/debt/libSceDiscId.md new file mode 100644 index 0000000000..fb26a7a344 --- /dev/null +++ b/docs/dev/debt/libSceDiscId.md @@ -0,0 +1,5 @@ +# libSceDiscId technical debt + +### Unknown function info + +- [sceDiscIdGet and sceDiscIdGet2](../../../core/libs/prx/libSceDiscId/Export.cpp) (libSceDiscId) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceDiscId.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceDiscMap.md b/docs/dev/debt/libSceDiscMap.md index 29814e4aa3..4fcd7700ec 100644 --- a/docs/dev/debt/libSceDiscMap.md +++ b/docs/dev/debt/libSceDiscMap.md @@ -6,4 +6,5 @@ ### Unknown function info +- [sceDiscMapGetPackageSize](../../../core/libs/prx/libSceDiscMap/Export.cpp) (libSceDiscMap) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceDiscMap.c); the `(int64_t, int*, int*)` prototype follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/disc_map/disc_map.cpp#L12), is not measured on a console, and every call throws - [sceDiscMapIsRequestOnHDD, sceDiscMapUnknown_ioKMruft1ek](../../../core/libs/prx/libSceDiscMap/Export.cpp) (libSceDiscMap, NIDs `lbQKqsERhtE` and `ioKMruft1ek`) - `ioKMruft1ek` has no known name. The `(path, offset, size, result)` and `(path, offset, size, flags, out1, out2)` signatures follow [shadPS4 disc_map.cpp](https://github.com/shadps4-emu/shadPS4/blob/b2d7ec7d31f9ad9426db93afe079db8679b3e1e0/src/core/libraries/disc_map/disc_map.cpp); the widths of the `ioKMruft1ek` outputs are unknown (shadPS4 `int*`, [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/ps4_libscediscmap.pas) `PInt64`), so they are `void*`. The error code is the one both return; [shadPS4 #2686](https://github.com/shadps4-emu/shadPS4/pull/2686) reads the console library as checking an initialisation global first in each call and returning it while the global is clear. Not measured on a console diff --git a/docs/dev/debt/libSceErrorDialog.md b/docs/dev/debt/libSceErrorDialog.md index 3c596b30b0..1166c74424 100644 --- a/docs/dev/debt/libSceErrorDialog.md +++ b/docs/dev/debt/libSceErrorDialog.md @@ -3,3 +3,7 @@ ### Silent stubs - [libSceErrorDialog](../../../core/libs/prx/libSceErrorDialog/Export.cpp) - the error dialog runs the state machine but shows nothing; the error code passed to `sceErrorDialogOpen` is only logged + +### 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. diff --git a/docs/dev/debt/libSceFiber.md b/docs/dev/debt/libSceFiber.md index 2e52af3853..9ad010851f 100644 --- a/docs/dev/debt/libSceFiber.md +++ b/docs/dev/debt/libSceFiber.md @@ -6,5 +6,7 @@ ### Unknown function info +- [_sceFiberInitializeWithInternalOptionImpl](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - its signature follows shadPS4's [initialize implementations](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L150-L153); its export alias is mapped at [RegisterLib](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L548-L551). Not measured on a console; the throwing declaration only resolves the import. +- [_sceFiberAttachContextAndRun, _sceFiberAttachContextAndSwitch](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - signatures follow shadPS4's [run implementation](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L260-L261) and [switch implementation](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L336-L337); their export aliases are mapped at [RegisterLib](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L555-L563). Not measured on a console; the throwing declarations only resolve imports. - [_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/libSceGameUpdate.md b/docs/dev/debt/libSceGameUpdate.md index d82804dae6..c67a4a14a3 100644 --- a/docs/dev/debt/libSceGameUpdate.md +++ b/docs/dev/debt/libSceGameUpdate.md @@ -3,3 +3,4 @@ ### Unknown function info - [sceGameUpdateGetAddcontLatestVersion](../../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - a null `entitlement_label` returns `0x80412803` as in prosper; KytyPS5 accepts it instead, and neither is confirmed on a console. Bytes after `found` are zeroed as in KytyPS5; prosper leaves them untouched +- [sceGameUpdateCreateRequest, sceGameUpdateCheck, sceGameUpdateAbortRequest, sceGameUpdateDeleteRequest](../../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - an initialized `sceGameUpdateCreateRequest` returns `0x80D50001` and creates no request, so `sceGameUpdateCheck`, `sceGameUpdateAbortRequest` and `sceGameUpdateDeleteRequest` answer `0x80412805` for every id that passes their argument checks. The PS5 3.20 module, as read by prosper, keeps 8 requests with ids from `0x20000000`, and answers "no update" when its patch-check service reports `0x80412883`; what that service returns on an offline console is unknown. Closes when: measured on a PS5 diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index 1206760ae4..54a6bf362d 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -3,3 +3,165 @@ ### Silent stubs - [libSceHmd](../../../core/libs/prx/libSceHmd/Export.cpp) implements only the disconnected-headset path: initialization succeeds, device queries report `NotDetected`, and opening a device returns `DeviceDisconnected`. Headset support, tracking, rendering and additional HMD exports are not implemented; SDK-level ABI compatibility and in-game behaviour remain unverified. + +### Unknown function info + +- [sceHmdInternalGetPUVersion](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalEventInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetVirtualDisplaySize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSocialScreenSetOutput](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetVirtualDisplayDepth](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetEyeStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalAnotherScreenOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionUnsetCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetCrashReportCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetDebugTextMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdDistortionGetWorkMemoryAlign](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalRegisterEventCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdDistortionGetWorkMemorySize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStartWideNear](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetPUSerialNumber](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetDebugSocialScreenMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetM2LedBrightness](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetIPD](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalMirroringModeSetAspect](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuGetStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalAnotherScreenSendVideo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetRequiredPUPVersion](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionSetCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuCheckAfterPvt](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalResetInertialSensor](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionSetUserEventStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdDistortionSetOutputMinColor](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetVirtualDisplayDepth](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGetDistortionWorkMemoryAlign](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStartMultilayer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalPushVr2dData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionClearUserEventEnd](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetIPD](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetVRMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalCrashReportReadDataSize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetVr2dData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalAnotherScreenSendAudio](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetPuPositionParam](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionInitializeCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGet2DEyeOffset](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetDebugSocialScreenMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetHmuPowerStatusForDebug](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGetDistortionWorkMemorySize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionSetDisplayBuffers](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalAnotherScreenSetFadeAndSwitch](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalCreateSharedMemory](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalMirroringModeSetAspectDebug](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetPuBuildNumber](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdDistortionGetCompoundEyeCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetS3dPassMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStopCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalIsCommonDlgMiniAppVr2d](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalMmapGetSensorCalibrationData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdDistortionGetCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionQueryOnionBuffAlign](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetDebugMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalIsMiniAppVr2d](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalIsCommonDlgVr2d](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionSetOutputMinColor](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetPortConnection](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionDebugGetLastInfo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetM2LedOn](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGetFieldOfView](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionAddDisplayBuffer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetIpdSettingEnableForSystemService](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetLedOn](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternal3dAudioSendData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetDeviceInformationByHandle](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStopLiveCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalAnotherScreenClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalCrashReportReadData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetDeviceInformation](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStartCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionQueryGarlicBuffAlign](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetHmuPowerControl](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalBindDeviceWithUserId](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetHmuSerialNumber](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdDistortionGetWideNearCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalCheckS3dPassModeAvailable](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalAnotherScreenGetAudioStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSeparateModeGetAudioStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStartLiveCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalIsGameVr2d](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGetAssyError](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionFinalize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSocialScreenSetFadeAndSwitch](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetVirtualDisplayHeight](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdDistortionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuSetMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetForcedCrash](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuCheckPartialUpdateAvailable](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSeparateModeClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetDebugTextMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalCrashReportClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetHmuPowerControlForDebug](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetVirtualDisplayHeight](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalResetLedForVsh](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetDebugMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalCrashReportCancel](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSeparateModeGetVideoStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalCheckDeviceModelMk3](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdSetVr2dEnable](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetSidetone](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSeparateModeOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalMmapGetModeId](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdDistortionGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuSendSize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGetWideNearDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStartMultilayer2](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGetDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetDeviceConnection](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetVirtualDisplaySize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionUnsetDisplayBuffers](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetHmuOpticalParam](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSeparateModeSendVideo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalResetLedForVrTracker](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionQueryOnionBuffSize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStartWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionSetUserEventEnd](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalCrashReportOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetDemoMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStartWideNearWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGetDistortionParams](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuSend](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionClearUserEventStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetUserType](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetDemoMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalMapSharedMemory](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetCrashDumpInfo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetTv4kCapability](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuReset](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStart2dVr](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalMmapGetCount](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGetInertialSensorData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternal3dAudioClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetBrightness](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSocialScreenGetFadeState](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSeparateModeSendAudio](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetStatusReport](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetBrightness](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionFinalizeCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetPortStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStop](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalAnotherScreenGetVideoStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternal3dAudioOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalAnotherScreenGetFadeState](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetDebugGpo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetIpdSettingEnableForSystemService](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionDebugGetLastInfoMultilayer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetPuRevision](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionQueryGarlicBuffSize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index c08ca1948b..c4c8e094a2 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -6,7 +6,6 @@ ### Unknown function info -- PPSA29866 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2), [sceVideoOutSetFlipMaster and sceVideoOutClearFlipMaster](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut) - PPSA09030 imports from its `Media/Plugins/psvr2.prx` declared without parameters, signatures unknown, added without an implementation because the title fails to load the plugin without them: [sceHmd2ReprojectionGetStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - [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 - [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 @@ -17,3 +16,4 @@ ### 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 +- [sceHmd2ReprojectionQueryDisplayBufferSizeAlign, sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign, sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign and sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - each returns a `{uint64 size, uint64 alignment}` pair in RAX:RDX; the values were measured on a retail PS5 (firmware 13.60) with no PS VR2 headset from a homebrew payload that loaded `libSceHmd2.sprx` without initializing it, and stayed the same for every argument tried; `sceHmd2ReprojectionQueryBufferSizeAlign` measured there returns 0x100828 and 0x100 for every argument, including the 2, 0xAF and 1 that PPSA05666 passes. They are assumed not to depend on initialization or on a connected headset, and their parameters are unknown diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index cfcbd069b6..60c619625d 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -19,9 +19,60 @@ - [sceHttpSetAuthInfoCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature and callback type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h) (PS4); no PS5 reference has them. The `id` is not checked, as in the other libSceHttp setters - [sceHttpWaitRequest](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the timeout is assumed to be in microseconds and a negative timeout to wait without limit; the invalid-value error for a null handle or event array or a non-positive event count is assumed, not measured. No request is ever pending, so a wait returns 0 events when its timeout passes - [sceHttpUriCopy](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - read from the disassembly of libSceHttp.sprx on firmware 13.60, not measured on a console and not compared with other emulators. The arguments are (out, src, pool, prepare, require), `prepare` before `require`, the reverse of `sceHttpUriParse`. A null source returns `ERROR_INVALID_URL`; without an output or a pool, a null `require` returns `ERROR_INVALID_VALUE`, otherwise the size is only reported. Only the scheme, hostname, path, username and password are copied, in that order, each with its terminator (a missing one becomes an empty string and takes one byte), and the port; the query, the fragment and the opaque flag stay 0. A pool that is too small clears the output and the pool and returns `ERROR_OUT_OF_MEMORY` (0x80431022, the console's out-of-size error) +- [sceHttpAuthCacheImport](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpAuthCacheExport](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpDbgEnableProfile](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpDbgSetPrintf](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpDbgShowStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetMemoryPoolStats](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpDbgShowConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpDbgShowRequestStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpDbgShowMemoryPoolStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpDbgGetConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpDbgGetRequestStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpAbortRequestForce](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpAddRequestHeaderRaw](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpRemoveRequestHeader](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetAuthEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetAutoRedirect](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpTrySetNonblock](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetNonblock](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpTryGetNonblock](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetEpollId](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetEpollId](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetEpoll](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetCookie](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetCookieEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpAddCookie](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetCookieSendCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpCookieExport](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpCookieImport](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetCookieTotalMaxSize](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetCookieMaxSize](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetCookieMaxNum](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetCookieMaxNumPerDomain](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetCookieStats](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpsEnableOptionPrivate](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpsDisableOptionPrivate](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetPriorityOption](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetPolicyOption](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpRequestGetAllHeaders](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetAcceptEncodingGZIPEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetAcceptEncodingGZIPEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetDefaultAcceptEncodingGZIPEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetProxy](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetSocketCreationCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetHttp09Enabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetRegisteredCtxIds](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpAddQuery](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetDelayBuildRequestEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpCacheRedirectedConnectionEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature ### Functional +- [sceHttpUriParse, sceHttpUriBuild, sceHttpUriMerge](../../../core/libs/prx/libSceHttp/src/Uri.cpp) - console behavior for non-ASCII scheme and hostname bytes remains unmeasured. ASCII character classification and scheme case folding follow [RFC 3986 sections 3.1 and 3.2](https://www.rfc-editor.org/rfc/rfc3986#section-3.1), independently of the host locale; parsing retains the existing C-locale rejection of those bytes. + - [sceHttpUriEscape](../../../core/libs/prx/libSceHttp/src/Uri.cpp) escapes every byte except the ASCII unreserved set in [RFC 3986 section 2.3](https://www.rfc-editor.org/rfc/rfc3986#section-2.3), independently of the host locale. Console behaviour for non-ASCII input remains unmeasured. - [sceHttpAuthCacheFlush, sceHttpCookieFlush](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the context-id signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs), and the NIDs `zzB0StvRab4` and `seCvUt91WHY` match [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp). The offline backend never stores authentication credentials or cookies, so flushing a live context succeeds with empty caches. Calls return `BEFORE_INIT` (0x80431001) when no context is alive and `INVALID_ID` (0x80431100) for an unknown, deleted or non-context handle, following the existing context lifecycle checks. These error codes and their precedence have not been measured on PS5 hardware; shadPS4 only stubs these calls. Network-backed caches remain unimplemented. diff --git a/docs/dev/debt/libSceHttpCache.md b/docs/dev/debt/libSceHttpCache.md new file mode 100644 index 0000000000..7c93d10351 --- /dev/null +++ b/docs/dev/debt/libSceHttpCache.md @@ -0,0 +1,5 @@ +# libSceHttpCache technical debt + +### Unknown function info + +- [sceHttpCacheClear, sceHttpCacheClearAll, sceHttpCacheCompleteRequest, sceHttpCacheCreateRequest, sceHttpCacheCreateRequestWithTag, sceHttpCacheDeleteRequest, sceHttpCacheInit, sceHttpCacheReadData, sceHttpCacheRetrieve, sceHttpCacheRetrieveWithMemoryPool, sceHttpCacheRevalidate, sceHttpCacheRevalidateWithMemoryPool, sceHttpCacheSetCacheSharing, sceHttpCacheSetData, sceHttpCacheSetQuota, sceHttpCacheSetResponseHeader, sceHttpCacheSystemClearAll, sceHttpCacheSystemInit, sceHttpCacheSystemSendStatistics, sceHttpCacheSystemShutdown, sceHttpCacheSystemTerm and sceHttpCacheTerm](../../../core/libs/prx/libSceHttpCache/Export.cpp) (libSceHttpCache) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHttpCache.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 76178899b3..9d4d0abbdd 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -2,15 +2,60 @@ ### Silent stubs -- [sceImeOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) shows no panel: the text is typed on the host keyboard, which [ImeTextInput](../../../core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp) forwards from the game window (SDL text input, Backspace, Delete, Left, Right, Home, End, Enter and Escape), and the controller cannot type. While a panel is open, key presses do not reach the keyboard-to-pad bindings of [PadInput](../../../core/libs/prx/libSceVideoOut/src/PadInput.cpp), as the console's panel takes the controller; key releases still do. Minecraft Preview (PPSA19639, #1277) opens it when entering a world +- [sceImeOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) shows no panel: the text is typed on the host keyboard, which [ImeTextInput](../../../core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp) forwards from the game window (SDL text input, Backspace, Delete, Left, Right, Home, End, Enter and Escape), and the controller cannot type. While a panel is open, key presses do not reach the keyboard-to-pad bindings of [PadInput](../../../core/libs/prx/libSceVideoOut/src/PadInput.cpp), as the console's panel takes the controller; key releases still do. Minecraft Preview (PPSA19639, #1277) opens it when entering a world. The external keyboard filter is accepted and never called because no keycode events are produced ### Unknown function info +- [sceImeSetConsecutiveClauseConversionEnabled](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - name from its NID (`row9sfnYNx4`) in [sce_symbols](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); neither [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0ebea15c24253b60f514fc37a348dabc8d0bcb90/src/core/libraries/ime/ime.cpp) nor [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/e5f0ee7fc2fb4ce88c6abb2fa02296404e2fe2a6/src/libs/ime.cpp) implements it, so it throws. PPSA24191 calls it with 1 as its only argument before each `sceImeOpen` and opens the panel only when it returns 0; the argument type, the result while a panel is open and the error codes are unknown - [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. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title -- [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`. A text filter or an external keyboard filter throws, as their contract is unknown. `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 +- [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 +- [sceImeVshOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeKeyboardUpdate](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshUpdate](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshUpdateContext](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshClearPreedit](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshNotifySetText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshSetCaretGeometry](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshConfirmPreedit](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshSetCaretIndexInPreedit](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshSetPreeditGeometry](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshSetSelectGeometry](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeSetCandidateIndex](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeConfirmCandidate](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeDisableController](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeSetVoiceDictationEnabled](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeSetEditEventWithRemotePlayEnabled](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeForTestFunction](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshInformConfirmdString2](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshSetSelectionText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeCheckUpdateTextInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeCheckRemoteEventParam](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeKeyboardOpenInternal](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshSetParam](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshUpdateContext2](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshSendTextInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshInformConfirmdString](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshNotifyCurrentTextInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshNotifyCurrentTextInfoForReconvertText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshNotifyDeleteText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshNotifyUpdateCaretText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshCheckAutoReConvertingPreeditText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshSetPanelPosition](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshNotifyPreeditMetaInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeConfigSet](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeConfigGet](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeDicGetWords](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeDicAddWord](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeDicDeleteWord](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeDicReplaceWord](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeDicDeleteLearnDics](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeDicDeleteUserDics](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature ### 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/main/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 - [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/libSceImeDialog.md b/docs/dev/debt/libSceImeDialog.md index 01003701d8..6e641b31ce 100644 --- a/docs/dev/debt/libSceImeDialog.md +++ b/docs/dev/debt/libSceImeDialog.md @@ -6,4 +6,5 @@ ### Unknown function info +- [sceImeDialogForTestFunction, sceImeDialogForceClose, sceImeDialogGetCurrentStarState, sceImeDialogInitInternal, sceImeDialogSetPanelPosition](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - their signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/ime_dialog.h#L34-L51), not measured on a console. They are declared without parameters solely to resolve known imports; this is not an ABI claim, and every call throws. - [sceImeDialogInit, sceImeDialogGetStatus, sceImeDialogGetResult, sceImeDialogAbort, sceImeDialogTerm](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - the statuses (none 0, running 1, finished 2), the end statuses (ok 0, user canceled 1, aborted 2) and the errors of `sceImeDialogInit` for an open dialog (`0x80bc0001`), a null parameter (`0x80bc0031`), a maximum text length outside 1 to 2048 (`0x80bc0016`) and a null text buffer (`0x80bc0017`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b2d7ec7d31f9ad9426db93afe079db8679b3e1e0/src/core/libraries/ime/ime_dialog.cpp); none is measured on a PS5. A result, abort or term on a dialog that is not finished, running or open returns `0x80bc0002`, where shadPS4 returns `0x80bc0105` to `0x80bc0107`, and `sceImeDialogTerm` also closes a running dialog, which shadPS4 refuses. The text reaches the title's buffer only when Enter accepts it diff --git a/docs/dev/debt/libSceJpegDec.md b/docs/dev/debt/libSceJpegDec.md index 342b2df171..833ad70e91 100644 --- a/docs/dev/debt/libSceJpegDec.md +++ b/docs/dev/debt/libSceJpegDec.md @@ -2,8 +2,9 @@ ### Unknown function info +- [sceJpegDecDecodeWithInputControl](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceJpegDec.c), but no credible prototype was found. It is declared without parameters solely to resolve known imports; this is not an ABI claim - [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - structure layouts follow [OpenOrbis `jpeg.h`](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/_types/jpeg.h#L13-L72) and [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L20-L117); attributes 1 and 2 are the values [SharpProspero's `JpegImage.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Graphics/JpegImage.cs#L64-L74) passes. The error codes are libScePngDec's: no reference lists the JpegDec ones, and SharpProspero raises 0x80650020 on its JpegDec path where it raises libScePngDec's 0x80690020 on its PngDec path. Image info always reports color space 3, suitable CSC attribute 1 and zero component ids and sampling factors; the console's values, including which images it reports attribute 2 for, are unknown. The coefficient memory size is 0 because the host decoder needs no coefficient buffer; the size the console reports for progressive images is unknown. `max_image_width` is not checked, and the packed `width << 16 | height` decode result is libScePngDec's ### Functional -- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - decode modes other than 0 and down scales other than 1 throw +- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - decode modes other than 0 and down scales other than 1, 2, 4 and 8 throw, as does a down scale of 2, 4 or 8 on a CMYK or YCCK image, which libjpeg-turbo cannot decode to RGB. The down scale factors are the ones [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L57-L58) documents; the output size is the source size divided by the factor and rounded up, and the pixels come from libjpeg-turbo's scaled inverse DCT. The console's rounding, pixel values and the packed decode result for a down-scaled image (the output size here) are unverified diff --git a/docs/dev/debt/libSceKeyboard.md b/docs/dev/debt/libSceKeyboard.md index 40900b0e78..a0a894689e 100644 --- a/docs/dev/debt/libSceKeyboard.md +++ b/docs/dev/debt/libSceKeyboard.md @@ -6,4 +6,5 @@ ### Unknown function info +- [sceDbgKeyboardClose, sceDbgKeyboardGetKey2Char, sceDbgKeyboardInit, sceDbgKeyboardOpen, sceDbgKeyboardRead, sceDbgKeyboardReadState](../../../core/libs/prx/libSceKeyboard/Export.cpp) (libSceKeyboard) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceKeyboard.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. - [sceKeyboardConnectPort, sceKeyboardDebugGetDeviceId, sceKeyboardDeviceOpen, sceKeyboardDisconnectAll, sceKeyboardDisconnectDevice, sceKeyboardDisconnectPort, sceKeyboardGetConnection, sceKeyboardGetDeviceInfo, sceKeyboardGetHandle, sceKeyboardMbusInit, sceKeyboardPadEmulateClose, sceKeyboardPadEmulateOpen, sceKeyboardSetProcessFocus, sceKeyboardSetProcessPrivilege](../../../core/libs/prx/libSceKeyboard/Export.cpp) (libSceKeyboard) - throwing stubs declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index 7b5f325b65..ff8c271f1f 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -10,7 +10,7 @@ - [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) follows FreeBSD 11's `lib/libc/amd64/gen/makecontext.c`: at most six arguments, a stack of at least `MINSIGSTKSZ` (2048) bytes, and `exit(0)` when the function returns with no `uc_link`. The console's version is not verified - [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) on Windows pins its stack writable until a later `makecontext` stack overlaps it, since nothing tells the library when the guest frees the stack - [Thread exit destructors](../../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped -- [fdopendir](../../../core/libs/prx/libSceLibcInternal/Directory.cpp) (libSceLibcInternal) - the directory stream starts at the directory's first entry, while FreeBSD's [`fdopendir`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/opendir.c#L67-L74) reads from the descriptor's current offset, and `FD_CLOEXEC` is not set on the descriptor. As in FreeBSD, a descriptor that is not open fails with `EBADF`, one that is not a directory with `ENOTDIR` (both leave it open), and `closedir` closes it +- [fdopendir](../../../core/libs/prx/libSceLibcInternal/Directory.cpp) (libSceLibcInternal) follows FreeBSD 11's [`fdopendir`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/opendir.c#L67-L74): it sets `FD_CLOEXEC` on the descriptor first, and the stream starts at the descriptor's current position instead of the first entry. On Windows the stream skips the entries the descriptor's cursor (`lseek` or `getdirentries`) has passed, which assumes that the descriptor and the stream list a directory in the same host order; on Linux the stream is opened on a duplicate of the descriptor, which shares its position. The Linux path is not run by the tests here. As in FreeBSD, a descriptor that is not open fails with `EBADF`, one that is not a directory with `ENOTDIR` (both leave it open, and `FD_CLOEXEC` stays set on the latter), and `closedir` closes it - [fstatvfs](../../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run) - [posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) create no process. FreeBSD 11.4 returns the error the child's exec leaves ([`_posix_spawn_thr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/posix_spawn.c#L227-L231), [`do_posix_spawn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/posix_spawn.c#L295-L303)), and `posix_spawnp` searches `PATH`, or `_PATH_DEFPATH` when it is unset, ending with `ENOENT` when no directory holds the program ([`_execvpe`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/exec.c#L272-L281), [`execvPe`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/exec.c#L257-L260)). So when the program is not in the guest file system both return `ENOENT` and leave the process ID unwritten; when it is there they throw. PPSA99305 (Dusklight) reaches this when SDL looks for `zenity` to show a file dialog. The attributes and file actions are not applied, so one that would fail in the child is not reported, and whether the console's application sandbox lets the child be created at all (FreeBSD returns that error first) is unverified. The attribute and file action calls keep FreeBSD 11.4's objects. Null or uninitialized objects, which FreeBSD dereferences, throw. - [getopt, getopt_long](../../../core/libs/prx/libSceLibcInternal/Getopt.cpp) (libSceLibcInternal) follow FreeBSD 11 [getopt.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt.c) and [getopt_long.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt_long.c) with `GNU_COMPATIBLE`: `getopt` does not permute, `getopt_long` permutes unless `POSIXLY_CORRECT` is set in the guest environment or the option string starts with `+`, and the exported `opterr`, `optind`, `optopt`, `optreset` and `optarg` start at 1, 1, 0, 0 and null. Warnings go to the guest's current `stderr` prefixed with `__progname`, where FreeBSD's `warnx` keeps the `stderr` of its first call. A null `getopt` option string, a null argument vector or argument, a negative `optind` and an `optind` moved inside a pending permutation throw where FreeBSD faults. `getopt_long_only` is not exported. That the console's `libSceLibcInternal` uses this FreeBSD code is unverified diff --git a/docs/dev/debt/libSceLoginDialog.md b/docs/dev/debt/libSceLoginDialog.md index c2c48d3e93..07a38ff414 100644 --- a/docs/dev/debt/libSceLoginDialog.md +++ b/docs/dev/debt/libSceLoginDialog.md @@ -4,3 +4,7 @@ - [libSceLoginDialog](../../../core/libs/prx/libSceLoginDialog/Export.cpp) - the login dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled - [sceLoginDialogClose](../../../core/libs/prx/libSceLoginDialog/Export.cpp) - sceLoginDialogClose transitions an initialized dialog directly to COMMON_DIALOG_STATUS_FINISHED even when called before Open, following the Signin / NetCtlAp / PlayerSelection convention; this behavior is unverified against real hardware. + +### Unknown function info + +- [sceLoginDialogOpen, sceLoginDialogGetResult](../../../core/libs/prx/libSceLoginDialog/Export.cpp) - the 0x8134xxxx codes, the Open param checks (size 0x40, word 1 below 2, words 0x2C to 0x3C zero) and the `{1, 0x81340007}` result words are the ones [prosper](https://github.com/mattias800/prosper/blob/ed42e499888d5a86792c3e1b2df3aebd8ca1a681/prosper/src/hle/service/hle_service.cpp#L805-L874) reads from the shipped module. The order of the param and state checks follows prosper's handlers and is not stated separately, the meaning of word 1 and of the second result word is unknown, words 2 to 10 are not checked, and Open's busy result is never reached because the dialog never runs. Closes when: the module's check order is read or measured on a PS5 diff --git a/docs/dev/debt/libSceMbus.md b/docs/dev/debt/libSceMbus.md index e46de1ffb3..8c7cb7dd7e 100644 --- a/docs/dev/debt/libSceMbus.md +++ b/docs/dev/debt/libSceMbus.md @@ -7,3 +7,5 @@ ### Unknown function info - [libSceMbus](../../../core/libs/prx/libSceMbus/Export.cpp) (`libSceDeviceService`) - signatures taken from PPSA01317, which calls `sceDeviceServiceInitialize(3, 0)`, polls `sceDeviceServiceGetEventState(1)` and queries again when bit 0 or 1 is set, and calls `sceDeviceServiceQueryDeviceInfo_(0x7001, 0, 0, infos, 1, &count, &unknown, 0x70)`, treating a negative result as an error and `count` as the number of entries. The host has no devices: the event state is 0 and a query returns 0 with `count` 0 + +- [libSceMbus firmware 3.20 pending exports](../../../core/libs/prx/libSceMbus/PendingExports.cpp) - 115 symbol names are present in [the PS5 3.20 libSceMbus export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMbus.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`. diff --git a/docs/dev/debt/libSceMouse.md b/docs/dev/debt/libSceMouse.md index a26decfa8d..19fe9ab3b5 100644 --- a/docs/dev/debt/libSceMouse.md +++ b/docs/dev/debt/libSceMouse.md @@ -1,5 +1,9 @@ # libSceMouse technical debt +### Unknown function info + +- [sceMouseConnectPort, sceMouseDebugGetDeviceId, sceMouseDeviceOpen, sceMouseDisconnectDevice, sceMouseDisconnectPort, sceMouseGetDeviceInfo, sceMouseMbusInit, sceMouseSetHandType, sceMouseSetPointerSpeed and sceMouseSetProcessPrivilege](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMouse.c); the no-argument prototypes follow [shadPS4's PS4 stubs](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/mouse/mouse.cpp#L34-L146), not measured on a console. These declarations have no parameters solely to resolve known imports and make no ABI claim. + ### Functional - [sceMouseResetAbsolutePosition](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) - signature taken from a call site (a handle, then x and y as 16-bit values: PPSA30601 passes the centre of its display size); behaviour not measured on hardware. Validates initialization and the handle like `sceMouseRead` and returns `MOUSE_OK`; the position is not stored because the library reports relative motion only diff --git a/docs/dev/debt/libSceMove.md b/docs/dev/debt/libSceMove.md new file mode 100644 index 0000000000..ec7666f106 --- /dev/null +++ b/docs/dev/debt/libSceMove.md @@ -0,0 +1,10 @@ +# libSceMove technical debt + +### Silent stubs + +- [libSceMove](../../../core/libs/prx/libSceMove/Export.cpp) implements only the no-controller path: initialization and termination succeed, `sceMoveOpen` returns a new handle, `sceMoveClose` and `sceMoveResetLightSphere` succeed for an open handle, and the device info, state, extension port, vibration and `sceMoveSetLightSphere` calls return 1 (no controller connected) and write nothing + +### Unknown function info + +- [libSceMove](../../../core/libs/prx/libSceMove/Export.cpp) (libSceMove) - behaviour, error codes and the handle values (`0x30B0100`, then every `0x100`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/move/move.cpp) (PS4), which checks no handle or open arguments; none is measured on a PS5. Signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), except `sceMoveGetExtensionPortInfo`, which follows shadPS4. Before `sceMoveInit` and after `sceMoveTerm` every other call returns `NOT_INIT` (`0x80EE0001`); a null output or a negative timestamp returns `INVALID_ARG` (`0x80EE0003`). Closes when: measured on a PS5 +- [libSceMove](../../../core/libs/prx/libSceMove/Export.cpp) (libSceMove) - a handle that was never opened, is closed or was opened before `sceMoveTerm` throws: the console's `INVALID_HANDLE` (`0x80EE0004`) result and its order against `INVALID_ARG` are not known. `sceMoveOpen` throws for a user other than the initial user and for a type and index that are already open, whose console results are not known, and accepts any type and index. Closes when: measured on a PS5 diff --git a/docs/dev/debt/libSceMoveTracker.md b/docs/dev/debt/libSceMoveTracker.md new file mode 100644 index 0000000000..156c1dfb0a --- /dev/null +++ b/docs/dev/debt/libSceMoveTracker.md @@ -0,0 +1,5 @@ +# libSceMoveTracker technical debt + +### Unknown function info + +- [sceMoveTrackerCalibrateReset, sceMoveTrackerCalibrateYaw, sceMoveTrackerCameraUpdate, sceMoveTrackerCaptureInit, sceMoveTrackerCaptureTerm, sceMoveTrackerControllersUpdate, sceMoveTrackerGetState, sceMoveTrackerGetVersion, sceMoveTrackerGetWorkingMemorySize, sceMoveTrackerInit, sceMoveTrackerPlayGetImages, sceMoveTrackerPlayGetStates, sceMoveTrackerPlayStart, sceMoveTrackerPlayStep, sceMoveTrackerRecordSave, sceMoveTrackerRecordStart, sceMoveTrackerRecordStop and sceMoveTrackerTerm](../../../core/libs/prx/libSceMoveTracker/Export.cpp) (libSceMoveTracker) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMoveTracker.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceNKWeb.md b/docs/dev/debt/libSceNKWeb.md new file mode 100644 index 0000000000..4191fc13a3 --- /dev/null +++ b/docs/dev/debt/libSceNKWeb.md @@ -0,0 +1,5 @@ +# libSceNKWeb technical debt + +### Unknown function info + +- [sceNKWebInitialize and sceNKWebTerminate](../../../core/libs/prx/libSceNKWeb/Export.cpp) (libSceNKWeb) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNKWeb.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 06832be45a..a091bd2810 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -6,8 +6,9 @@ ### Unknown function info +- [sceNetEtherStrton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - its signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L737-L913) and its [Net bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs#L19), not measured on a console; the throwing declaration only resolves the import. - [sceHttp2GetMemoryPoolStats, sceSslGetMemoryPoolStats, sceNetGetMemoryPoolStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceHttp2, libSceSsl, libSceNet) - the signatures `(int id, stats*)`, the layouts (pool size, peak and current use, and for Http2 and Ssl a 32-bit reserved field) and the error codes were measured on a PS5 (firmware 13.60). The pool size is the one given at init or pool creation. Http2 and Net allocate nothing from these pools. Ssl counts its certificate backend allocations and returned data against its pool; C++ bookkeeping is outside that budget. All three initially report zero use, where the console already reports 0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool. Ssl's allocation sizes and peak usage do not reproduce the console allocator -- [sceNetClearDnsCache](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetClearDnsCache](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) returns 0 without doing anything: every `sceNetResolverStart*` lookup goes to the host resolver, so the library keeps no DNS cache to drop, and the host's own cache is left alone. The `(int flags)` signature and its meaning come from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L928-L930); nonzero flags throw because their meaning is unknown - [sceNetGetStatisticsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetInterfaceStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetSetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -16,7 +17,6 @@ - [sceNetShowPolicyWithMemory](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetShowRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetShowRouteWithMemory](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetResolverStartAton6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetResolverConnectAbort](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetResolverConnectCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetResolverConnect](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -40,19 +40,19 @@ - [sceNetGetIfList](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetIfListEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetIfListOnce](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetIfName](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetGetIfName](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(uint32_t index)` signature and the table of ten interface names (`lo0`, `eth0`, `eth1`, `dbg0`, `wlan0`, `wlan1`, `gbe0`, `bt0`, `phone0`, `pppoe0`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); an index past the table sets `EINVAL` and returns an empty string. Whether the console has more names, or returns null, is unverified on hardware - [sceNetGetIfnameNumList](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetNameToIndex](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(uint32_t* output)` signature and the `EINVAL` failure for a null output follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); the value comes from the host's `std::random_device`, as `kern.rng_pseudo` in libkernel does. Unverified on hardware - [sceNetGetRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetsockoptEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetSystemTime](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unverified on PS5 hardware: the `void(uint64_t*)` signature, process-relative microseconds and ignored null output follow [shadPS4 net.cpp](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1654-L1659). The clock is the same as `sceKernelGetProcessTime`, including `APS5_TIME_SCALE` - [sceNetGetSystemMemInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInitParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp), which forwards it to `sceNetInetPton` and does not know the flags either. Flags 0 converts as `sceNetInetPton` does; any other flags throw, since their meaning (an IPv6 scope or otherwise) is not known. Unverified on hardware +- [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); IPv4 conversion and the ignored flags follow PS4 firmware 10.71, not yet confirmed on PS5. There IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at the first space, tab or VT), except that a part starting with `09` is decimal (`"0989"` is 989) and an octal part with `8` or `9` fails. Flags 0, each single bit up to 0x100, 0x10000, 0x80000000 and -1 gave the same results; other values were not measured. LF, FF and CR as terminators, and `NET_EINVAL` for a null `src` or `dst`, follow `sceNetInetPton` and were not measured for this function. IPv6 converts as `sceNetInetPton` does. - [sceNetIoctl](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetMemoryAllocate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetMemoryAllocate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int64_t size, int flags)` signature, flag 2 as zero fill and the null result for a size of 0 or less follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp). The block comes from the guest heap, and an allocation failure returns null without setting an errno. Any other flag bit throws, since its meaning is not known. Unverified on hardware +- [sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(void* memory)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); the block returns to the guest heap and a null pointer is ignored. Unverified on hardware - [sceNetSetDnsInfoToKernel](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetSetsockoptEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetSocketInternal](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -180,6 +180,7 @@ ### Functional +- [sceNetResolverStartAton6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reverse-resolves a 16-byte IPv6 address through the host resolver (`getnameinfo` with `NI_NAMEREQD`) into the caller's buffer of `len` bytes, as `sceNetResolverStartAton` does for IPv4. The `(rid, addr, hostname, len, timeout, retry, flags)` signature comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L992-L997). An address with no name, or a name longer than `len - 1` bytes, fails with `RESOLVER_ENODNS` (`0x804101E1`) and leaves the buffer untouched; whether the console truncates instead is not verified. Abort flag 2 (ATON preservation) fails the next call with `EINTR` as it does `sceNetResolverStartAton`. Timeout and retry are ignored, and any nonzero flags throw because their meaning is unknown - [sceNetAccept](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) inherits the listener's non-blocking mode through the existing `SO_NBIO` setter, as the libkernel socket path does and FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L571) copies the socket state. The mode is sampled when accept starts; changes between connection creation and accept are not modelled. Not measured on a console. - [sceNetConnect to AF_UNSPEC](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had @@ -188,8 +189,9 @@ - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) stores `SO_ONESBCAST` (`0x10000`, [shadPS4's `ORBIS_NET_SO_ONESBCAST`](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net_types.h)) per socket and `sceNetGetsockopt` reads back the value that was set, as shadPS4 does; PPSA34501 sets it and crashes later when it fails with `EOPNOTSUPP`. Sends ignore it. FreeBSD 11.4's [ip(4)](https://man.freebsd.org/cgi/man.cgi?manpath=FreeBSD+11.4-RELEASE&query=ip&sektion=4) documents the like-named `IPPROTO_IP` option `IP_ONESBCAST`: a datagram sent to an interface's directed broadcast address, such as 192.168.2.255, leaves through that interface with its destination rewritten to 255.255.255.255, and only on a socket allowed to set `SO_BROADCAST`. Here host routing selects the outgoing interface and the destination is not rewritten. shadPS4 also leaves sends unchanged, and its [TODO](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp#L2467-L2470) reads the option the other way round: a send to 255.255.255.255 would be converted to the interface's broadcast address. Whether the console's `SOL_SOCKET` option behaves like `IP_ONESBCAST`, and what it reads back, is not verified on hardware. FreeBSD 11.4's `sogetopt()` reads several boolean socket options back as their flag bit (`so_options & optname`, `sys/kern/uipc_socket.c`), not as the value set; whether the console does so for `0x10000` is unknown - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) set `TCP_NODELAY` (`IPPROTO_TCP`, 1) on the host socket of a stream socket and read it back as `TF_NODELAY` (4) or 0, as FreeBSD 11.4's [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1626-L1650) does. Every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_NOPUSH` and `TCP_KEEPIDLE` among them, which FreeBSD implements) fails with `ENOPROTOOPT`, and the `IPPROTO_TCP` level on a socket that is not a stream socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/ip_output.c#L943-L944) answers it. A set on a connection that FreeBSD's [`tcp_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1498-L1502) refuses with `ECONNRESET` (refused, reset or in `TIME_WAIT`) succeeds here because the host accepts it. A null value with a nonzero length fails with `EINVAL`, where FreeBSD's `kern_setsockopt` returns `EFAULT`, and a get into less than an `int` fails with `EINVAL`, where `sooptcopyout` truncates. None of this was measured on a console. +- [sceNetGetSockInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reports 0 for the name, pid, policy, priority, queue lengths, virtual ports, state, bandwidth, drops and waits, sets no status flag except `SELF` (0x1) and `NONBLOCK` (0x200000), and reports the host's buffer sizes where none was set. An IPv6 socket fails with `EAFNOSUPPORT` and is not counted or listed; shadPS4 lists it with zero addresses. `flags` bits other than 0x40 fail with `EINVAL`, where shadPS4 ignores `flags`; 0x40 adds sockets in TIME_WAIT ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs)), but closed sockets are not kept. Otherwise the `s` below 0 count and list, the return value and `EINVAL` for a null `info` or an `n` below 1 follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/06e813ff8c8e2a6d81b02cabfcc9543c3debf78e/src/core/libraries/net/net.cpp#L1498-L1522). Not measured on a console. - [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 -- [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) in `sceNetSend`, `sceNetSendto`, `sceNetRecv`, `sceNetRecvfrom`, `sceNetRecvmsg` and `sceNetSendmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`); the value `0x80` is FreeBSD's (`sys/sys/socket.h`) and [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/net/net_types.h#L104). Winsock has no per-call non-blocking flag, so on Windows these calls still fail with `EOPNOTSUPP` for it +- [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) in `sceNetSend`, `sceNetSendto`, `sceNetRecv`, `sceNetRecvfrom`, `sceNetRecvmsg` and `sceNetSendmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`); the value `0x80` is FreeBSD's (`sys/sys/socket.h`) and [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/net/net_types.h#L104). Winsock has no per-call non-blocking flag, so on Windows the call first asks `WSAPoll` with a zero timeout whether the socket is ready and fails with `EAGAIN` when it is not. The check and the transfer are two host calls: a second thread that takes the data in between can leave a `sceNetRecv` blocked, and a `sceNetSend` of more bytes than the socket buffer holds can still block until it has sent them, where FreeBSD's `sosend_generic` returns a partial count - [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does - [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. diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 0df9af2742..99aa976623 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -8,6 +8,77 @@ - [sceNetCtlGetInfoV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `Jy1EO5GdlcM`) - imported by PPSA08587, which fails to load without it, and not called before its loading screen; declared without parameters as a throwing stub, signature unknown. Name from the aerolib NID list - [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 +- [sceNetCtlRegisterCallbackForNpToolkit](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlRegisterCallbackIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlRegisterCallbackForLibIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlUnregisterCallbackForNpToolkit](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlUnregisterCallbackIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlUnregisterCallbackForLibIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlCheckCallbackForNpToolkit](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlCheckCallbackForLibIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlClearEventIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlClearEventForNpToolkit](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlClearEventForLibIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetResultIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetStateIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetState2IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetEtherLinkMode](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetWifiType](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlConnectIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlConnectWithRetryIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlConnectConfIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlDisconnectIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetScanInfoBssidIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetScanInfoByBssidIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetScanInfoForSsidScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetScanInfoBssidForSsidListScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetScanInfoForSsidListScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlSetErrorNotificationEnabledIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlRegisterCallbackV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlUnregisterCallbackV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetResultV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetResultV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetStateV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetInfoV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetNatInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlSetStunWithPaddingFlagIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlUnsetStunWithPaddingFlagIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlIsBandwidthManagementEnabledIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlDisableBandwidthManagementIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlEnableBandwidthManagementIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetBandwidthInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetNetEvConfigInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetConnectionElapsedTimeIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpClearEvent](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpRegisterCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpUnregisterCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpGetResult](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpGetState](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpStart](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApCpStart](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApAppStartWithRetryPid](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApAppStartWithRetry](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpStartWithRetry](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpStartConf](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpStop](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApCpStop](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApAppInitWpaKey](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApAppInitWpaKeyForQa](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRestart](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetBweCheckCallbackIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetBweClearEventIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetBweRegisterCallbackIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetBweUnregisterCallbackIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetBweGetInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetBweStartInternetConnectionTestIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetBweFinishInternetConnectionTestIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetBweStartInternetConnectionTestBandwidthTestIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetBweSetInternetConnectionTestResultIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetWifiInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature ### Functional diff --git a/docs/dev/debt/libSceNetCtlAp.md b/docs/dev/debt/libSceNetCtlAp.md new file mode 100644 index 0000000000..012a82c476 --- /dev/null +++ b/docs/dev/debt/libSceNetCtlAp.md @@ -0,0 +1,12 @@ +# libSceNetCtlAp technical debt + +### Functional + +- [libSceNetCtlAp](../../../core/libs/prx/libSceNetCtlAp/Export.cpp) (libSceNetCtlAp) - the access point never starts: a title starts it through [libSceNetCtlApDialog](libSceNetCtlApDialog.md), which always reports the user as having cancelled. `sceNetCtlApGetState` reports Stopped (0), `sceNetCtlApGetInfo`, `sceNetCtlApGetConnectInfo` and `sceNetCtlApStop` return `NOT_STARTED` (`0x8041298F`) without writing, and no callback ever fires. Signatures, states, structure sizes (152 and 184 bytes), the 8-callback limit and error codes come from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/NetCtlAp.cs) + +### Unknown function info + +- [sceNetCtlApInit, sceNetCtlApTerm](../../../core/libs/prx/libSceNetCtlAp/Export.cpp) (libSceNetCtlAp) - every other call returns `NOT_INITIALIZED` (`0x80412994`) before `sceNetCtlApInit` and after `sceNetCtlApTerm`, which also drops the registered callbacks. A second `sceNetCtlApInit` throws, as no error for it is documented +- [sceNetCtlApGetInfo, sceNetCtlApGetConnectInfo, sceNetCtlApStop](../../../core/libs/prx/libSceNetCtlAp/Export.cpp) (libSceNetCtlAp) - checks run in this order, which is not verified: a null structure returns `INVALID_ADDR` (`0x80412991`), a size field other than 152 or 184 `INVALID_SIZE` (`0x80412998`), then `NOT_STARTED`. The option string is not read, since the accepted strings (`INVALID_OPT_STR`, `0x80412901`) are unknown. Whether `sceNetCtlApStop` of a stopped access point returns `NOT_STARTED` or 0 is unverified +- [sceNetCtlApClearEvent, sceNetCtlApGetResult](../../../core/libs/prx/libSceNetCtlAp/Export.cpp) (libSceNetCtlAp) - event types 1 to 4 are accepted and others return `INVALID_TYPE` (`0x80412990`), checked before the null `errorCode` (`INVALID_ADDR`). With no event ever delivered, `sceNetCtlApGetResult` writes 0; what the console writes before the first event is unverified +- [sceNetCtlApRegisterCallback, sceNetCtlApUnregisterCallback](../../../core/libs/prx/libSceNetCtlAp/Export.cpp) (libSceNetCtlAp) - a null function or id pointer returns `INVALID_ADDR`, a ninth registration `CALLBACK_MAX` (`0x80412995`), an id outside 0 to 7 `INVALID_ID` (`0x80412996`) and an unregistered id `ID_NOT_FOUND` (`0x80412997`). Ids are the lowest free slot, as in libSceNetCtl; the console's id assignment is unverified diff --git a/docs/dev/debt/libSceNgs2.md b/docs/dev/debt/libSceNgs2.md new file mode 100644 index 0000000000..548dfd5035 --- /dev/null +++ b/docs/dev/debt/libSceNgs2.md @@ -0,0 +1,6 @@ +# libSceNgs2 technical debt + +### Unknown function info + +- [sceNgs2FftInit, sceNgs2FftProcess, sceNgs2FftQuerySize](../../../core/libs/prx/libSceNgs2/PendingExports.cpp) (libSceNgs2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNgs2.c), but no credible prototypes were found in the cited sources. They are declared without parameters solely to resolve known imports and make no ABI claim. +- [sceNgs2CustomRackGetModuleInfo, sceNgs2GetWaveformFrameInfo, sceNgs2ParseWaveformUser, sceNgs2RackGetUserData, sceNgs2RackSetUserData, sceNgs2SystemEnumHandles, sceNgs2SystemEnumRackHandles, sceNgs2VoiceGetMatrixInfo, sceNgs2VoiceGetOwner](../../../core/libs/prx/libSceNgs2/PendingExports.cpp) (libSceNgs2) - names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNgs2.c); signatures follow [SharpProspero's Ngs2 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs), not measured on a console. Opaque structure pointers use `void*`; these stubs throw for import resolution. diff --git a/docs/dev/debt/libSceNpAuth.md b/docs/dev/debt/libSceNpAuth.md index 8e344dfda7..bb10e4b7d8 100644 --- a/docs/dev/debt/libSceNpAuth.md +++ b/docs/dev/debt/libSceNpAuth.md @@ -2,6 +2,8 @@ ### Unknown function info +- [sceNpAuthGetAuthorizationCodeA](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpAuth.c); its signature follows [shadPS4 `np_auth.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L155-L190), is not measured on a console, and the throwing declaration only resolves the import +- [sceNpAuthGetIdTokenA](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpAuth.c); its prototype follows [shadPS4 `np_auth.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L340-L375), is not measured on a console, and the throwing declaration only resolves the import - [sceNpAuthGetAuthorizedAppCode](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - declared without parameters and returns `SCE_NP_ERROR_SIGNED_OUT` without reading them, as there is no PSN sign-in; the request parameter and code output are not known. Minecraft (PPSA17221) imports it, fails to load without it, and after its sign-in dialog treats the signed-out code as offline play. -- [sceNpAuthAbortRequest, sceNpAuthGetAuthorizationCodeV3, sceNpAuthGetIdTokenV3, sceNpAuthPollAsync, sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - request tracking, `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` (0x80550306) on unknown request 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 repeated calls on completed requests, and `ORBIS_NP_AUTH_ERROR_ABORTED` (0x80550304) after an abort follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_auth.cpp) (PS4); async requests return OK and defer `SCE_NP_ERROR_SIGNED_OUT` to `sceNpAuthPollAsync`/`sceNpAuthWaitAsync` while sync requests fail immediately; PlayStation Network is not contacted. The parameter `size` fields are not checked (`ORBIS_NP_AUTH_ERROR_INVALID_SIZE`, structure layouts unknown), the request table has no limit (`ORBIS_NP_AUTH_ERROR_REQUEST_MAX`), and console behavior on PS5 is unverified +- [sceNpAuthAbortRequest, sceNpAuthGetAuthorizationCode, sceNpAuthGetAuthorizationCodeV3, sceNpAuthGetIdToken, 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/main/src/core/libraries/np/np_auth.cpp) (PS4); async requests defer `SCE_NP_ERROR_SIGNED_OUT` to Poll/Wait while sync requests fail immediately; PSN is not contacted. Parameter size validation and PS5 behavior are unverified - [sceNpAuthSetTimeout](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - imported by PPSA01317, which fails to load without it. The signature `(reqId, resolveRetry, resolveTimeout, connTimeout, sendTimeout, recvTimeout)` follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_auth.cpp) (PS4), which only stubs it. An unknown request ID returns `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` like the other request calls; otherwise it returns OK and keeps nothing, as no request reaches the network the timeouts would bound. Argument checks of the console are unverified diff --git a/docs/dev/debt/libSceNpCommerce.md b/docs/dev/debt/libSceNpCommerce.md index 7a9d0ec642..8de5c9ca21 100644 --- a/docs/dev/debt/libSceNpCommerce.md +++ b/docs/dev/debt/libSceNpCommerce.md @@ -6,4 +6,5 @@ ### Unknown function info +- [sceNpCommerceDialogInitializeInternal](../../../core/libs/prx/libSceNpCommerce/Export.cpp) (libSceNpCommerce) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpCommerce.c); the no-parameter prototype follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_commerce/np_commerce.cpp#L61) and is not measured on a console. It is declared without parameters solely to resolve known imports; this is not an ABI claim, and every call throws. - [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/Export.cpp) (libSceNpCommerce) - declared without parameters; follows [prosper](https://github.com/mattias800/prosper/blob/73413cb445b0364bb49c4d2478e9ee1614b9f7d9/prosper/src/hle/np/np.cpp#L500-L504), which serves it with its `sceNpCommerceDialogOpen` handler: every argument is ignored, nothing is validated, it returns 0 and the dialog finishes at once, so `sceNpCommerceDialogGetResult` reports it cancelled. The parameters and the console's error results are unknown. Call of Duty: Black Ops (PPSA34501) calls it at Zombies > Online diff --git a/docs/dev/debt/libSceNpCppWebApi.md b/docs/dev/debt/libSceNpCppWebApi.md index 4ea6a15b4e..9352e4f30e 100644 --- a/docs/dev/debt/libSceNpCppWebApi.md +++ b/docs/dev/debt/libSceNpCppWebApi.md @@ -14,3 +14,4 @@ - [sce::Np::CppWebApi::Common::String](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the 0x20-byte layout (32-bit reference count, buffer at 0x08, buffer size at 0x10, `LibContext*` at 0x18) comes from the module bundled with one title (PPSA21564); other SDK versions are not checked. `append` grows the buffer with the host allocator and the destructor frees it, instead of going through the `LibContext` allocator. `pushBack` into `Vector` stores a deep copy, so a destroyed temporary cannot invalidate the stored element; a title that byte-copies a `String` with a live buffer would still double-free, as nothing increments the reference count - [sce::Np::CppWebApi::Common::Vector](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671, whose dump ships no `libSceNpCppWebApi.prx`. A `Vector` handle is an `IntrusivePtr` over a shared body; bodies created by `pushBack` use a host-side layout with the reference count at 0x30, matching the adopted-body convention measured on PPSA21564. The pointer/deleter constructor adopts a title-provided body released through the stored deleter; `pushBack` on such a body throws, since its element layout is unknown. `begin`, `end`, iterators and `operator[]` throw: the only `Vector`s the title reads back come from failed transactions, which never produce one - [sce::Np::CppWebApi::SessionManager2V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671. Offline, the factories, `PlayerSessionsApi` calls and transaction `start`/`getResponse`/`readData` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter setters, `initialize`, `terminate`, constructors and destructors succeed without keeping state, as no request is ever sent. Response-body accessors (`getSessionId`, `getPlayerSessions`, `getInvitations`, the `IsSet` queries) throw: a failed request never produces a response object +- [libSceNpCppWebApi](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 73 exports are named from aerolib, so each name recomputes to the NID a title imports (`tools/nid_names.py`), and each throws. They are the `Common` template members (`IntrusivePtr`, `Vector`, `Iterator`, `ConstIterator`, `Transaction`, `TransactionBase`) and the `TitleCloudStorage::V1` and `InGameCatalog::V5` accessors: a mangled name gives the class, the method and the argument types, but the layout of the object the call is made on, the values the caller reads back and the console's errors are unknown, so no call can be answered without writing to memory no source describes. No call site is recorded for any of them diff --git a/docs/dev/debt/libSceNpEntitlementAccess.md b/docs/dev/debt/libSceNpEntitlementAccess.md index b457b050c8..2a9d2f3089 100644 --- a/docs/dev/debt/libSceNpEntitlementAccess.md +++ b/docs/dev/debt/libSceNpEntitlementAccess.md @@ -2,6 +2,7 @@ ### Unknown function info +- [sceNpEntitlementAccessGetAddcontEntitlementInfoIndividual, sceNpEntitlementAccessPollConsumableEntitlementInfo, sceNpEntitlementAccessRequestConsumableEntitlementInfo, sceNpEntitlementAccessRequestConsumeEntitlement](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) (libSceNpEntitlementAccess) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpEntitlementAccess.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim - [sceNpEntitlementAccessGetEntitlementKey](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - the three-argument signature, 16-byte key and errors `0x817D0002`/`0x817D0007` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp); PPSA01521 passes a service label, entitlement label and output pointer. Only the no-key path is implemented: local entitlement labels carry no keys, and the output is untouched on failure. Console error precedence and key provisioning are unverified - [sceNpEntitlementAccess{Request,Poll}{Unified,Service}EntitlementInfo{,List}](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - signatures unknown; no reference implements them, so they are declared without parameters. The single-item variants copy the behaviour of the list ones, with no source of their own. Like the consume requests in the same library, they report the signed-out NP state with `SCE_NP_ERROR_SIGNED_OUT` (`0x80550006`) and write nothing. PPSA15657 calls the unified request during start-up and continues past the error. Whether the console fails at the request or at the poll when signed out is unverified diff --git a/docs/dev/debt/libSceNpGameIntent.md b/docs/dev/debt/libSceNpGameIntent.md index 681c6dbf8f..91a214fd5e 100644 --- a/docs/dev/debt/libSceNpGameIntent.md +++ b/docs/dev/debt/libSceNpGameIntent.md @@ -6,5 +6,6 @@ ### Unknown function info +- [sceNpAppLauncherInitialize, sceNpAppLauncherLaunchApp, sceNpAppLauncherTerminate, 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), but no credible prototypes were found in the cited sources. They are declared without parameters solely to resolve known imports and make no ABI claim. - [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 diff --git a/docs/dev/debt/libSceNpSessionSignaling.md b/docs/dev/debt/libSceNpSessionSignaling.md index c23308b9fe..f5f1396a51 100644 --- a/docs/dev/debt/libSceNpSessionSignaling.md +++ b/docs/dev/debt/libSceNpSessionSignaling.md @@ -7,4 +7,4 @@ - [sceNpSessionSignalingGetConnectionStatistics](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature - [sceNpSessionSignalingGetConnectionStatus](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - `(int32 context id, int32 connection id, int32* status, SceNetInAddr* peer address, uint16* peer port)` is ASTRO BOT's (PPSA21564 01.007.000) call at 0x30622B: the context id from a 32-bit field, the connection id, three stack outputs, a negative result taken as an error, then the address passed to `sceNetInetNtop(AF_INET, ...)` and the port to `sceNetNtohs`. It matches PS4's `sceNpSignalingGetConnectionStatus`. The title reaches it only for a connection id from the signaling library, which offline never hands one out. No session ever activates offline, so every connection id answers `CONN_NOT_FOUND` (0x8055330C, from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/64d198190fa05d37cbb1fa29c50f967c376fce55/src/core/libraries/np/np_error.h)) without writing the outputs, and a null status answers `INVALID_ARGUMENT`. The context id is not checked, as contexts are not tracked. The console's answer for an unknown context or connection is unverified - PPSA31327 imports declared without parameters, signatures unknown: [sceNpSessionSignalingGetGroupInfo, sceNpSessionSignalingGetConnectionFromNetAddress2](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling), imported by Yakuza 0 Director's Cut and not called before the main menu. - +- [sceNpSessionSignalingCreateJoinActivate, sceNpSessionSignalingGetConnectionFromNetAddress, sceNpSessionSignalingGetCustomProperty, sceNpSessionSignalingGetGroupFromPeerAddress, sceNpSessionSignalingInitializeWithPort, sceNpSessionSignalingJoinActivate and sceNpSessionSignalingSetCustomProperty](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpSessionSignaling.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. diff --git a/docs/dev/debt/libSceNpWebApi2.md b/docs/dev/debt/libSceNpWebApi2.md index c805a3decc..cccf4a5c69 100644 --- a/docs/dev/debt/libSceNpWebApi2.md +++ b/docs/dev/debt/libSceNpWebApi2.md @@ -6,6 +6,9 @@ ### Unknown function info +- [sceNpWebApi2AddMultipartPart, sceNpWebApi2IntCreateRequest, sceNpWebApi2IntPushEventCreateCtxIndFilter and sceNpWebApi2SetMultipartContentType](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpWebApi2.c); no-argument prototypes follow [shadPS4's PS4 stubs](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_web_api2/np_web_api2.cpp), not measured on a console. These declarations have no parameters solely to resolve known imports and make no ABI claim. +- [sceNpWebApi2CreateMultipartRequest, sceNpWebApi2InitializeForPresence, sceNpWebApi2IntInitialize, sceNpWebApi2IntInitialize2 and sceNpWebApi2SendMultipartRequest](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpWebApi2.c). The signatures follow the PS4 implementations in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L52-L221) and [its multipart sender](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L388-L399); opaque struct pointers use `const void*` or `void*` to preserve source constness. These prototypes are not measured on a console. +- [sceNpWebApi2IntPushEventCreateDestPs4CtxIndFilter](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - no credible prototype was found; it is declared without parameters solely to resolve known imports and makes no ABI claim. - [sceNpWebApi2PushEventAbortHandle](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2, NID `1OLgvahaSco`) - the two signed 32-bit handle arguments and return type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/np/np_web_api2/np_web_api2.cpp) (PS4); the PS5 signature and abort behavior are unverified. PPSA01736 imports it. An unknown library context throws, as in `sceNpWebApi2PushEventSetHandleTimeout`. The handle is not checked and nothing is aborted, as push event handles are not tracked and nothing waits on a handle offline - [sceNpWebApi2GetMemoryPoolStats](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - imported by PPSA21703's Unity PSN plugin. The signature `(int libCtxId, stats*)` and the layout of the Http2 statistics above (measured on a PS5 for Http2, Ssl and Net) are assumed for this library, not measured. The pool size is the one given to `sceNpWebApi2Initialize`, and the peak and current use are 0. The error codes are unknown, so a null output or an unknown context throws - [sceNpWebApi2PushEventSetHandleTimeout](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - imported by PPSA01317, which fails to load without it. The signature `(int libCtxId, int handleId, uint32_t timeout)` follows `sceNpWebApiSetHandleTimeout` of the PS4 libSceNpWebApi in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api.cpp) and is unverified on the PS5. An unknown library context throws, as in `sceNpWebApi2GetMemoryPoolStats`, since its error code is unknown. The handle is not checked, as push event handles are not tracked (`sceNpWebApi2PushEventDeleteHandle` accepts any handle), and the timeout is not kept, as nothing waits on a handle offline diff --git a/docs/dev/debt/libSceOpusCeltDec.md b/docs/dev/debt/libSceOpusCeltDec.md index 53d4b848d7..66de7596b2 100644 --- a/docs/dev/debt/libSceOpusCeltDec.md +++ b/docs/dev/debt/libSceOpusCeltDec.md @@ -2,6 +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. - [sceOpusCeltDecCreateEx, sceOpusCeltDecDecode, sceOpusCeltDecDestroy, sceOpusCeltDecGetSize, sceOpusCeltDecInitialize, sceOpusCeltDecTerminate](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) - imported by PPSA99001. Their signatures are assumed to match libSceOpusDec's in [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp), and the 640-byte state size, context handling and byte-count return are taken from [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp); none of them was measured on a console. PPSA99001 opens this decoder only as a fallback when libSceOpusDec reports a retryable error for a CELT packet, which libSceOpusDec here never returns, so the fallback is not reached ### Functional diff --git a/docs/dev/debt/libSceOpusCeltEnc.md b/docs/dev/debt/libSceOpusCeltEnc.md new file mode 100644 index 0000000000..6f232c9cee --- /dev/null +++ b/docs/dev/debt/libSceOpusCeltEnc.md @@ -0,0 +1,5 @@ +# libSceOpusCeltEnc technical debt + +### Unknown function info + +- [sceOpusCeltEncCreate, sceOpusCeltEncCreateEx, sceOpusCeltEncCtl, sceOpusCeltEncDestroy, sceOpusCeltEncEncode, sceOpusCeltEncEncodeFloat, sceOpusCeltEncEncodeFloatWithPriority, sceOpusCeltEncEncodeWithPriority, sceOpusCeltEncGetSize, sceOpusCeltEncInitialize and sceOpusCeltEncTerminate](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) (libSceOpusCeltEnc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusCeltEnc.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceOpusDec.md b/docs/dev/debt/libSceOpusDec.md index 9273851ee1..fa6aa1dfd1 100644 --- a/docs/dev/debt/libSceOpusDec.md +++ b/docs/dev/debt/libSceOpusDec.md @@ -1,5 +1,9 @@ # libSceOpusDec technical debt +### 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. + ### Functional - [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for mono/stereo signed-16 decoding at the creation rates the research lists: 8000, 12000, 16000, 24000 and 48000 Hz. The six signatures, 640-byte opaque state allocation and byte-count return follow [public PS5 decoder research](https://github.com/blackbearreloaded/ps5-hardware-audio-decoding/blob/26be5a12f92b2cdc67f8a9e082fd69ae0e9a35a2/docs/DECODING.md#direct-opus-abi) and [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). Console PCM equivalence and error codes are unverified. Other sample rates, packet loss concealment, invalid inputs and termination with live decoders throw. Context identifiers and host decoder state are private registries; the firmware's opaque state layout and error recovery are unknown. diff --git a/docs/dev/debt/libSceOpusSilkEnc.md b/docs/dev/debt/libSceOpusSilkEnc.md new file mode 100644 index 0000000000..3e129c1e1f --- /dev/null +++ b/docs/dev/debt/libSceOpusSilkEnc.md @@ -0,0 +1,5 @@ +# libSceOpusSilkEnc technical debt + +### Unknown function info + +- [sceOpusSilkEncCancel, sceOpusSilkEncCreate, sceOpusSilkEncCreateEx, sceOpusSilkEncCtl, sceOpusSilkEncDestroy, sceOpusSilkEncEncode, sceOpusSilkEncEncodeFloat, sceOpusSilkEncEncodeFloatWithPriority, sceOpusSilkEncEncodeFloatWithPriorityAndTimeout, sceOpusSilkEncEncodeWithPriority, sceOpusSilkEncEncodeWithPriorityAndTimeout, sceOpusSilkEncGetSize, sceOpusSilkEncInitialize and sceOpusSilkEncTerminate](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) (libSceOpusSilkEnc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusSilkEnc.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libScePlayGo.md b/docs/dev/debt/libScePlayGo.md index ef94e4f939..e1834da340 100644 --- a/docs/dev/debt/libScePlayGo.md +++ b/docs/dev/debt/libScePlayGo.md @@ -2,5 +2,7 @@ ### Unknown function info -- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file, a package with `sce_sys/playgo-scenario.json` (PPSA01748, whose dump lists no chunks) accepts every chunk id as installed while `scePlayGoGetChunkId` lists only chunk 0. A package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts chunk 0; querying any other id throws with a message naming the missing `playgo-chunk.dat` and `playgo-scenario.json`, since the incomplete dump cannot establish whether that id exists; `APS5_PLAYGO_UNLISTED_CHUNKS=installed` answers every such id as installed (`LOCAL_FAST`, which PPSA15388 needs to load its saves) and `=absent` answers `SCE_PLAYGO_ERROR_BAD_CHUNK_ID` (which ends PPSA03001's id walk), as the user's choice for that dump. A `sce_sys` that holds other `playgo-*` files (`playgo-ficm.dat`, `playgo-hash-table.dat`) without `playgo-chunk.dat` or `playgo-scenario.json`, or a `playgo-chunk.dat` without a `plgx` header and a nonzero count, throws naming the file, because no such dump is known. These answers are unverified on a console, as is the console's answer for an id outside a package's chunk table. +- [scePlayGoRequestNextChunk, scePlayGoSnapshot](../../../core/libs/prx/libScePlayGo/Export.cpp) (libScePlayGo) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePlayGo.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim +- [scePlayGoSetLanguageMask](../../../core/libs/prx/libScePlayGo/Export.cpp) (libScePlayGo) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePlayGo.c); the `(uint32_t, uint64_t)` prototype follows [ChonkyStation4 `ScePlayGo.cpp`](https://github.com/liuk7071/ChonkyStation4/blob/c101a23ecbc15e56c6e30d5dd24d3ed8880a957a/ChonkyStation4/OS/Libraries/ScePlayGo/ScePlayGo.cpp#L240), is not measured on a console, and every call throws +- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file, a package with `sce_sys/playgo-scenario.json` (PPSA01748, whose dump lists no chunks) accepts every chunk id as installed, and `scePlayGoGetChunkId` lists ids 0 to 999. A title that builds its set of installed chunks from that list skips any chunk it does not see. The console's chunk limit is not known here, and a package with more chunks would see ids from 1000 up as not listed. A package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts chunk 0; querying any other id throws with a message naming the missing `playgo-chunk.dat` and `playgo-scenario.json`, since the incomplete dump cannot establish whether that id exists; `APS5_PLAYGO_UNLISTED_CHUNKS=installed` answers every such id as installed (`LOCAL_FAST`, which PPSA15388 needs to load its saves) and `=absent` answers `SCE_PLAYGO_ERROR_BAD_CHUNK_ID` (which ends PPSA03001's id walk), as the user's choice for that dump. A `sce_sys` that holds other `playgo-*` files (`playgo-ficm.dat`, `playgo-hash-table.dat`) without `playgo-chunk.dat` or `playgo-scenario.json`, or a `playgo-chunk.dat` without a `plgx` header and a nonzero count, throws naming the file, because no such dump is known. These answers are unverified on a console, as is the console's answer for an id outside a package's chunk table. - [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - `scePlayGoGetSupportedOptionalChunk` and `scePlayGoGetOptionalChunk` report every bit set for any `type`, i.e. every optional chunk (language pack) supported and installed, like `scePlayGoGetLanguageMask`. The bit layout and the values of `type` are not known; PPSA01748 calls both with type 0 and hides every text language listed in `playgo-scenario.json` `chunkSupportedLanguages` when the bitmask is 0. The console's answer for a package that ships no optional chunks is unverified. diff --git a/docs/dev/debt/libScePngDec.md b/docs/dev/debt/libScePngDec.md index 5aece02801..2eca53adb5 100644 --- a/docs/dev/debt/libScePngDec.md +++ b/docs/dev/debt/libScePngDec.md @@ -3,3 +3,4 @@ ### Unknown function info - [scePngDecQueryMemorySize, scePngDecCreate](../../../core/libs/prx/libScePngDec/Export.cpp) (libScePngDec) - the work memory is a fixed 0x20 bytes, because decoding uses host memory; the PS5 module asks for `((max_image_width + 0x47) & ~7) * (8 << attribute)` plus a constant, as read from the 3.20 module by prosper. A null parameter returns `0x80690003`; prosper returns `0x80690001` when it cannot read the parameter, which was not measured. Closes when: measured on a PS5 +- [scePngDecDecodeWithInputControl](../../../core/libs/prx/libScePngDec/Export.cpp) (libScePngDec) - its name and NID are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePngDec.c); the no-parameter prototype follows [shadPS4's pngdec.h](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/libpng/pngdec.h#L76) and is not measured on a console. It is declared without parameters solely to resolve known imports; this is not an ABI claim. diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index 39dc328d7f..3fc3155064 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -10,7 +10,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, 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 on other firmware and title contexts are unavailable. - [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - only shadPS4 (PS4) has bodies. A null tick returns `INVALID_POINTER` like the existing `sceRtcFormatRFC3339`, where shadPS4 formats the current time instead; offsets outside ±1439 minutes return `INVALID_VALUE` like `sceRtcFormatRFC3339`; the local-time variants use the host's offset at the given instant (shadPS4 uses `sceKernelGettimezone`, the current offset) - [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 -- [sceRtcGetCurrentRawNetworkTick, sceRtcGetCurrentAdNetworkTick, sceRtcGetCurrentDebugNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console reads clocks 0x13, 0x12 and 0x11 (the ad and debug clocks fall back to the network clock 0x10) and returns the clock time plus the Unix epoch in ticks, or `0x80B50001` when the clock is unsupported (errno 5). Those console clocks are not modelled; all three return the system clock like `sceRtcGetCurrentNetworkTick`. 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) - the console rejects a null pointer with `0x80B50002` and a tick before the Unix epoch with `0x80B50003`, then sets the system clock (clock 0, 0x10, 0x12 or 0x11) from the tick. A valid tick throws: the host clock is not set. sceRtcSetConf passes its third and fourth arguments to the same clock call with clock 0 and throws; what the pair selects is unknown +- [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 + +- The process-local clocks set by [sceRtcSetCurrentTick, sceRtcSetCurrentNetworkTick, sceRtcSetCurrentAdNetworkTick and sceRtcSetCurrentDebugNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) are visible only through libSceRtc. The system clock and libkernel clock APIs keep reporting host time; cross-library clock consistency is not modelled. The console's clock interactions are not measured diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index b161448f8f..19757221c0 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -12,4 +12,5 @@ - [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. The `uint16_t*` parameter is inferred: the measurement does not show how many bytes the console writes, PPSA03490 passes a pointer to a 16-bit value and [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.cpp) declares the PS3 `cellRudpSetMaxSegmentSize(u16 mss)`, so only 16 bits are written - [sceRudpGetOption](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) takes `(int ctx_id, int option, void* value, uint32_t len)`, taken from the bundled `Media/Plugins/SceRudp.prx` of PPSA06888: its two exported wrappers pass their first three arguments through unchanged and set `len` to 4, the same shape as its `sceRudpSetOption` call, which passes a 4-byte value. The option codes, the defaults and the error results are unknown, so it still throws - PPSA06888 imports with unknown signatures: [sceRudpGetRemoteInfo](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by its bundled `Media/Plugins/SceRudp.prx` and not called during start-up. The third argument is a pointer to a 4-byte buffer seeded from the byte its wrapper reads through the second argument, which does not show what the arguments mean. The managed `SceRudp.Context.GetRemoteInfo()` in the title's Unity metadata takes no parameters and returns a `SceRudp.AddrAndVport`; its native arguments are not visible there. Names from the NID table of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp), which declares them without parameters too -- PPSA31327 imports declared without parameters, signatures unknown: [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by Yakuza 0 Director's Cut and not called before the main menu. Names from the NID table of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) +- [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. The signatures, the 8-byte `SceRudpPollEvent`, 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. diff --git a/docs/dev/debt/libSceS3da.md b/docs/dev/debt/libSceS3da.md new file mode 100644 index 0000000000..c941424735 --- /dev/null +++ b/docs/dev/debt/libSceS3da.md @@ -0,0 +1,5 @@ +# libSceS3da technical debt + +### Unknown function info + +- [sceS3daClose, sceS3daGetInfo, sceS3daOpen and sceS3daSendData](../../../core/libs/prx/libSceS3da/Export.cpp) (libSceS3da) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceS3da.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index ad4a4bf0b4..92ab2a4581 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -10,6 +10,7 @@ - [sceSaveDataCancel](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA23126, which fails to load without it; added without an implementation because its signature is unknown. Every save data call here finishes before it returns, so nothing is ever in flight to cancel, but what the console returns then is unknown - [sceSaveDataDirNameSearch](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. A save's size is the `blocks` of the `sceSaveDataMount3` call that created it, kept beside the save in `.blocks`; a save without a kept size takes that of its next mount that names one, and otherwise reports `SAVE_DATA_BLOCKS_MAX`. The search infos (blocks, free blocks and 32 reserved bytes per returned name, following shadPS4's `OrbisSaveDataSearchInfo`), `sceSaveDataGetMountInfo` and the two block keys use that size and, as free, what the save's files leave of it, each file rounded up to 32 KiB blocks; the console's file system overhead is not known and not counted. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it. In the directory name pattern `%` matches any run of characters, none included, and `_` exactly one, as the comments of shadPS4's matcher describe; also not confirmed on hardware - [sceSaveDataInitialize3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown +- [sceSaveDataCheckIpmiIfSize, sceSaveDataDebug, sceSaveDataDebugCheckBackupData, sceSaveDataDebugCleanMount, sceSaveDataDebugCompiledSdkVersion, sceSaveDataDebugCreateSaveDataRoot, sceSaveDataDebugProspero, sceSaveDataDebugRemoveSaveDataRoot and sceSaveDataDebugTarget](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSaveData.native.c). The seven no-argument prototypes follow the PS4 stubs in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/save_data/savedata.cpp#L653-L725), not measured on a console; no credible prototypes were found for the other two. All declarations have no parameters solely to resolve known imports and make no ABI claim. ### Functional @@ -17,6 +18,6 @@ - [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. - [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 -- [sceSaveDataGetMountInfo, sceSaveDataGetParam](../../../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 +- [sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataSetParam](../../../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 - [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/libSceScreenShot.md b/docs/dev/debt/libSceScreenShot.md index 89867a84f6..568cb899b6 100644 --- a/docs/dev/debt/libSceScreenShot.md +++ b/docs/dev/debt/libSceScreenShot.md @@ -2,4 +2,5 @@ ### Unknown function info +- [sceScreenShotCapture, sceScreenShotDisable, sceScreenShotDisableNotification, sceScreenShotEnable, sceScreenShotEnableNotification, sceScreenShotGetAppInfo, sceScreenShotGetDrcParam, sceScreenShotIsDisabled, sceScreenShotIsVshScreenCaptureDisabled, sceScreenShotSetDrcParam and sceScreenShotSetParam](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceScreenShot.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceScreenShotSetOverlayImage and sceScreenShotSetOverlayImageWithOrigin](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceScreenShot.c). The PS4 compatibility implementation also exposes the names but declares no parameters; no PS5 prototype was found. The declarations have no parameters solely to resolve known imports and make no ABI claim diff --git a/docs/dev/debt/libSceShellUIUtil.md b/docs/dev/debt/libSceShellUIUtil.md new file mode 100644 index 0000000000..f07f584e14 --- /dev/null +++ b/docs/dev/debt/libSceShellUIUtil.md @@ -0,0 +1,5 @@ +# libSceShellUIUtil technical debt + +### Unknown function info + +- [sceShellUIUtilCreateTaskToken, sceShellUIUtilGetAppUrl, sceShellUIUtilInitialize, sceShellUIUtilLaunchByUri, sceShellUIUtilLaunchTask and sceShellUIUtilTerminate](../../../core/libs/prx/libSceShellUIUtil/Export.cpp) (libSceShellUIUtil) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceShellUIUtil.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceSigninDialog.md b/docs/dev/debt/libSceSigninDialog.md index 9c05ac5649..5353db25a6 100644 --- a/docs/dev/debt/libSceSigninDialog.md +++ b/docs/dev/debt/libSceSigninDialog.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceSigninDialogClose, sceSigninDialogGetResult](../../../core/libs/prx/libSceSigninDialog/Export.cpp) (libSceSigninDialog) - SceSigninDialogResult layout (int32 result and 3 reserved int32 words, zeroed) follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/dialog.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/SigninDialog/SigninDialog.cs); error codes follow libSceLoginDialog's 0x80B8xxxx common-dialog codes rather than KytyPS5's 0x8135xxxx, and offline calls finish as user cancelled without opening a dialog. Neither choice is verified on hardware +- [sceSigninDialogClose, sceSigninDialogGetResult](../../../core/libs/prx/libSceSigninDialog/Export.cpp) (libSceSigninDialog) - SceSigninDialogResult layout (int32 result and 3 reserved int32 words, zeroed) follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/dialog.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/SigninDialog/SigninDialog.cs); error codes are the 0x80B8xxxx common-dialog codes rather than KytyPS5's 0x8135xxxx, and offline calls finish as user cancelled without opening a dialog. Neither choice is verified on hardware diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 8b1772a8a4..597a2246f7 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -2,9 +2,9 @@ ### Unknown function info -- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection` and that `sceSslRead` returns 0 at the end of the stream. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006), as `sceSslGetMemoryPoolStats` does on the console and as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L2688-L2720) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/29021b5a71bbc80f262d887e6ba18b3a72963f49/src/SharpEmu.Libs/Network/SslExports.cs#L41-L54) return for other context calls; for an unknown connection, a connection whose context was terminated and a null host the console's codes are unknown, so these throw. `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: SharpProspero's `SceSslVersion` ends at TLS 1.2, and Mbed TLS 1.3 needs PSA crypto state that is process-wide rather than allocated from a context's pool. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, its validity dates and the host name the connection was created with, which is also sent as SNI, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits and codes); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C). A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. The result of the first completed `sceSslConnect` is final for the connection: a failure sends close_notify, later `sceSslConnect` calls return the same code, and `sceSslRead` and `sceSslWrite` throw. With a verify callback set it is called once after the handshake, outside the connection's lock, with the connection id, that code (0 when the chain passed), handles to a copy of the peer chain that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated), the chain length and the user argument; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and any other return accepts the connection whatever the code was, following SharpProspero's "a negative return fails the handshake" ([https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252)). Whether the id is the context or the connection, whether the second argument is that code or the bits of the failed checks, the chain element type, when the console runs the callback and whether a non-negative return overrides a failed check are unknown. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking. A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_READ_TIMEOUT` (0x80951710) from `sceSslConnect` and `sceSslRead`, and the call can be repeated; one whose `SO_SNDTIMEO` expires throws, as no write timeout code is known. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SSL_ERROR_SOCKET_CLOSED` (0x8095170D), as does a transport that ends without close_notify during the handshake, and a fatal alert returns `SSL_ERROR_FATAL_ALERT` (0x8095F010); when one of these ends the handshake the session is dropped and the next `sceSslConnect` starts over. Which situation returns which code is inferred: [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/errno.h#L512-L546) gives the names and values but not when the console returns them, so `WANT_POLLIN` and `WANT_POLLOUT` rather than `SSL_ERROR_EAGAIN` (0x8095F00F) for a socket that would block, `SOCKET_CLOSED` rather than `SSL_ERROR_EOF` (0x8095177C) for a handshake cut short, `READ_TIMEOUT` for an expired receive timeout, `FATAL_ALERT` for a fatal alert, `INVALID_CERT` (0x8095F00A) for a verification failure no option names, and a repeated `sceSslConnect` returning the first result, 0 included, rather than an error are all unverified on a console. Other handshake and socket failures, a read or write before the handshake, after a failed verification or after `sceSslClose`, a descriptor that is not a socket, a null buffer, write flags other than 0 and read flags other than 0 and `MsgFlagPartialRead` (1) throw, as their console codes are unknown; `MsgFlagPeek` (3) is not implemented. `sceSslRead` returns what one Mbed TLS read delivers, up to `len`, with flags 0 and with `MsgFlagPartialRead` alike; SharpProspero describes `sceSslRead` as reading up to `len` but `MsgFlagPartialRead` as returning before the buffer is full, so whether flags 0 waits to fill the buffer is unknown. It returns 0 after the server's close_notify and also when the transport ends without one, as Mbed TLS reports both as the end of the stream. `sceSslWrite` sends all of `len`, capped at `INT_MAX`, and returns the count. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify. The socket is not checked when the connection is created, `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, and `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. None of this is verified on the console. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts -- [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - takes the connection id, as in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252). It sends close_notify once if the handshake completed, marks the connection closed so later `sceSslConnect`, `sceSslRead` and `sceSslWrite` throw, and leaves the socket to the title; on a connection that never connected, already failed verification or was already closed it returns 0. Whether the console closes the socket, what it returns for a connection that was never connected, and what happens when a non-blocking socket cannot take the close_notify at once (it is not retried) are unknown -- [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName, sceSslGetNotBefore, sceSslGetNotAfter, sceSslGetFingerprint](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and context ownership follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L293-L350), with hash types matching its `SceSslFingerprintHashAlgorithm` enum. Operations take a context handle as their first argument and operate on certificate and name handles scoped to that context. `sceSslGetSerialNumber` copies into a caller-allocated output buffer or queries the required length when the buffer is null or capacity is zero; a buffer smaller than the serial number returns `SSL_ERROR_INVALID_ARG` (0x8095177a). Mbed TLS parses the certificate's serial number, validity period (converted to microseconds since Gregorian year 0001 epoch), fingerprint digests and X.509 subject and issuer distinguished names, extracting OID attribute short names (or numeric strings) and value strings. An unknown context, unknown or expired certificate handle or invalid certificate name handle throws; null output pointers, unsupported hash types or out-of-range entry indices return `SSL_ERROR_INVALID_ARG` (0x8095177a). The error codes, the zero-capacity length query, the NUL-terminated truncation of name entry strings with the full value length in `valueLen`, and the lifetime of name handles (released only by `sceSslFreeSslCertName` or when the context is terminated) have not been measured on a PS5 +- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. The behaviour below was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title); what differs for a title process is unknown. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006) from `sceSslCreateConnection`, and every connection call on an unknown or deleted connection id or on a connection whose context was terminated returns `SSL_ERROR_INVALID_ID`. `sceSslCreateConnection` returns `SSL_ERROR_INVALID_VALUE` (0x8095F007) for a null host and does not check the socket: `sceSslConnect` on a descriptor that is not a socket returns `SCE_NET_ERROR_EBADF` (0x80410109). `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: the console offers TLS 1.2 only (its ClientHello carries 12 suites and no supported_versions extension) and a TLS 1.3-only server gets `SSL_ERROR_FATAL_ALERT` (0x8095F010) from it, as from this build. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`, whose connections return `SSL_ERROR_INVALID_ID` from then on. The console's pool use for one connection with a two-certificate chain peaked at 80,544 bytes in a 256 KiB pool (5,568 after `sceSslInit`, 12,000 after loading one CA); a 16 KiB pool fails the connect with `SSL_ERROR_OUT_OF_MEMORY` (0x809517D5) there and here. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, the host name the connection was created with (case-insensitively; an IP literal sends no SNI and matches an IP subject alternative name; an empty host sends an empty SNI and fails `CN_CHECK`) and, when enabled, its validity dates, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits, codes and initial options); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C), as on the console. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. A failed verification is final: close_notify is sent, later `sceSslConnect` calls return the same code (also after the `WANT_POLLIN` rounds of a non-blocking socket), and `sceSslRead` and `sceSslWrite` throw, as the console's codes for them were not measured. With a verify callback set it is called once after the handshake, outside the connection's lock, with the context id, the bits of the enabled checks that failed (`KNOWN_CA_CHECK` 0x20, `CN_CHECK` 0x4, the date checks when enabled, 0 when the chain passed), handles to a copy of the peer chain as received (the leaf first, the root last when the server sent it) that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated; the console frees them at the delete), the chain length and the user argument. A chain fault that no option names (`SSL_ERROR_INVALID_CERT` without a callback) throws when a callback is set, as the bits the console passes for it were not measured. A non-negative return accepts the connection whatever the bits were; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and the callback runs again, with the same chain, from every later `sceSslConnect`, `sceSslRead` and `sceSslWrite` of the connection, which return 0x8095F0FD while it keeps refusing and proceed once it accepts. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. `sceSslRead` and `sceSslWrite` before `sceSslConnect` run the handshake and verification themselves and return its code when it fails. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking (the console returned 32 KiB of a 64 KiB write). A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_WANT_POLLIN` as well, and the call can be repeated; `SSL_ERROR_READ_TIMEOUT` (0x80951710) is never returned. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` with flags 0 reads until `len` bytes arrived or the stream ended and with any other flags (`MsgFlagPartialRead` 1 measured, 2 behaved the same) returns what the first record delivers; a non-blocking read that got some bytes returns them. It returns 0 after the server's close_notify and after a transport that ends without one, every later read 0 again, and `SSL_ERROR_EOF` (0x8095177C) and `SSL_ERROR_SOCKET_CLOSED` (0x8095170D) never from a read. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) from `sceSslRead` and `SCE_NET_ERROR_EPIPE` (0x80410120) from `sceSslWrite`, on every later call too; a record that fails its MAC returns 0x80951D55 from `sceSslRead` after the fatal alert goes out. `sceSslWrite` accepts flags 0 only, `SSL_ERROR_INVALID_VALUE` otherwise, sends all of `len`, capped at `INT_MAX`, and returns the count; it keeps writing after the peer's close_notify or a stream that ended without one, as the console does. A fatal alert during the handshake returns `SSL_ERROR_FATAL_ALERT` and ends the transport: later `sceSslConnect` calls return 0, `sceSslRead` 0 and `sceSslWrite` `SSL_ERROR_SOCKET_CLOSED`, as measured. A transport that ends without close_notify or a reset during the handshake returns `SSL_ERROR_SOCKET_CLOSED`, the session is dropped and the next `sceSslConnect` starts over; those two handshake cases and `SSL_ERROR_INVALID_CERT` (0x8095F00A) for a verification failure no option names were not measured. Other handshake and socket failures, a read or write after a failed verification, a write after `sceSslClose`, a null buffer and a read or write from inside the callback throw, as their console codes are unknown. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify and returns `SSL_ERROR_INVALID_ID` when repeated. `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, which then returns `SSL_ERROR_INVALID_ID`; a second `sceSslTerm` returns `SSL_ERROR_INVALID_ID` and `sceSslInit` with a pool size of 0 returns `SCE_KERNEL_ERROR_EINVAL` (0x80020016). `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. The console's first `sceSslConnect` in a process took 23.8 s before its ClientHello left, a one-time initialisation not emulated. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts +- [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - takes the connection id, as in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252). Measured on 13.40 as the connection entry describes: it sends close_notify once if the handshake completed and returns 0, 0 again when repeated and 0 on a connection that never connected, already failed verification or lost its transport to a fatal alert; a close before the handshake changes nothing, so the connection can still connect. After the close `sceSslConnect` returns 0 and `sceSslRead` 0, as on the console; `sceSslWrite` after it throws, as the console's code was not measured. A close_notify that a non-blocking socket cannot take at once returns `SSL_ERROR_WANT_POLLOUT` (0x8095F014) and the close can be repeated; after a reset it returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) every time. The socket is left to the title; an unknown connection returns `SSL_ERROR_INVALID_ID` (0x8095F006). Whether the console closes the socket is unknown +- [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName, sceSslGetNotBefore, sceSslGetNotAfter, sceSslGetFingerprint](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and context ownership follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L293-L350), with hash types matching its `SceSslFingerprintHashAlgorithm` enum. Operations take a context handle as their first argument and operate on certificate and name handles scoped to that context. `sceSslGetSerialNumber` copies into a caller-allocated output buffer or queries the required length when the buffer is null or capacity is zero; a buffer smaller than the serial number returns `SSL_ERROR_INVALID_ARG` (0x8095177a). Mbed TLS parses the certificate's serial number, validity period (converted to microseconds since Gregorian year 0001 epoch), fingerprint digests and X.509 subject and issuer distinguished names, writing the attribute long names the console writes (`commonName`, `organizationName`, `countryName`, measured on 13.40 through the verify callback's chain handles; numeric strings for attributes Mbed TLS cannot name) and value strings with `valueLen` set to the value's length. An unknown context, unknown or expired certificate handle or invalid certificate name handle throws; null output pointers, unsupported hash types or out-of-range entry indices return `SSL_ERROR_INVALID_ARG` (0x8095177a). `sceSslGetSerialNumber` with a buffer large enough leaves `*sboLen` as the caller set it, as measured on 13.40 with a 32-byte buffer. The error codes, the zero-capacity length query, the NUL-terminated truncation of name entry strings, and the lifetime of name handles (released only by `sceSslFreeSslCertName` or when the context is terminated) have not been measured on a PS5 - [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate -- [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures. The verify options select which verification failures `sceSslConnect` reports (see the connection entry above), with the bit values SharpProspero's `SceSslVerifyOption` gives: `SERVER_VERIFY` 0x1, `CN_CHECK` 0x4, `NOT_AFTER_CHECK` 0x8, `NOT_BEFORE_CHECK` 0x10, `KNOWN_CA_CHECK` 0x20. The initial options are all five checks (0x3D), so a connection whose title never calls these fails closed; what the console starts with is unknown, and the one source, the HTTPS layer of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L191), starts its libSceHttp connections with `ssl_flags` 0xA7 (`SERVER_VERIFY`, `CLIENT_VERIFY`, `CN_CHECK`, `KNOWN_CA_CHECK`, SNI), evidence that verification is on by default rather than for these bits. A check that is disabled is not reported, whatever the chain. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values SharpProspero lists, 0 (none) to 5 (TLS 1.2), and throws for others; whether the console accepts a TLS 1.3 value is unknown. Every accepted value gives the same handshake, as Mbed TLS 3 speaks TLS 1.2 and nothing older and TLS 1.3 is compiled out. `sceSslSetAlpn` stores the string up to its terminator and throws for a null list; it is split on commas into the protocols offered in the handshake, as HTTP libraries write ALPN lists, and whether the console expects that or the TLS wire format is unknown. `sceSslGetAlpnSelected` returns the protocol the server selected, or null before the handshake and when none was selected; the pointer is owned by the connection. An unknown connection and a connection whose context was terminated throw, as for `sceSslSetVerifyCallback` +- [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures; the behaviour was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title). The verify options select which verification failures `sceSslConnect` reports (see the connection entry above), with the bit values SharpProspero's `SceSslVerifyOption` gives: `SERVER_VERIFY` 0x1, `CN_CHECK` 0x4, `NOT_AFTER_CHECK` 0x8, `NOT_BEFORE_CHECK` 0x10, `KNOWN_CA_CHECK` 0x20. The initial options are `SERVER_VERIFY`, `CN_CHECK` and `KNOWN_CA_CHECK` (0x25): the console accepted an expired and a not-yet-valid server certificate until 0x8 or 0x10 was enabled, which then returned `SSL_ERROR_NOT_AFTER_CHECK` (0x8095F00D) and `SSL_ERROR_NOT_BEFORE_CHECK` (0x8095F00E); whether 0x2 (`CLIENT_VERIFY`) starts enabled was not determined and it is kept off. A check that is disabled is not reported, whatever the chain. Bits outside 0x3F return `SSL_ERROR_INVALID_VALUE` (0x8095F007) from both calls. Disabling 0x1, 0x4 or 0x20 from the payload stopped its process through the kernel's debug exception path (`mDBG: Sending signal ... signo: 0xa0020101`, 6 of 6 attempts; disabling 0x2, 0x8 and 0x10 returned 0), so whether a title may clear them is unknown; this build clears them, as titles import the call. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values 2 (SSL 3.0) to 5 (TLS 1.2) and returns `SSL_ERROR_INVALID_VALUE` for 0, 1 and anything above 5; the console has no TLS 1.3 value. Every accepted value gives the same handshake, as Mbed TLS 3 speaks TLS 1.2 and nothing older and TLS 1.3 is compiled out, which matches the console offering TLS 1.2 only. `sceSslSetAlpn` stores the string up to its terminator, split on commas into the protocols offered in the handshake (the console sent `h2,http/1.1` as the two names and a wire-format string as one name), a second call replaces the list, an empty string returns `SSL_ERROR_BAD_LENGTH` (0x80951773), and a null list throws (not measured). `sceSslGetAlpnSelected` returns the protocol the server selected, still after `sceSslClose`, or null before the handshake and when none was selected; the pointer is owned by the connection. An unknown connection and a connection whose context was terminated return `SSL_ERROR_INVALID_ID` (0x8095F006) from the four setters and throw from `sceSslGetAlpnSelected`, whose console return was not measured - [sceSslGetPem](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - static inspection of PPSA28002 confirms the context-first four-argument call with a caller-owned buffer and an in/out 64-bit length. Mbed TLS serializes PEM including its trailing NUL. Buffers smaller than the required encoding throw; an unknown context or foreign or expired certificate handle throws. The exact console PEM length policy, error precedence and truncation behavior have not been measured on a PS5. Existing serial/name, validity and fingerprint readers and their documented uncertainties remain unchanged diff --git a/docs/dev/debt/libSceSysmodule.md b/docs/dev/debt/libSceSysmodule.md index 5f59554ebf..042c89ea84 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`, `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 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. ### Unknown function info @@ -10,6 +10,7 @@ - [kModuleTable](../../../core/libs/prx/libSceSysmodule/ModuleTable.hpp) (libSceSysmodule) - module IDs registered from decrypted PS5 firmware 1.00–3.20 sysmodule lookup tables: - User-space IDs (0x00000005, 0x0000010a, 0x0000010e, 0x0000011b, 0x00000125, 0x00000126, 0x00000127, 0x00000128, 0x0000012a, 0x0000012b, 0x00000130) and internal IDs (0x8000007b, 0x8000007e, 0x8000007f, 0x80000080, 0x80000082, 0x80000083, 0x80000084, 0x80000085, 0x80000086, 0x80000087, 0x80000088, 0x80000089, 0x8000008e, 0x80000090, 0x80000091, 0x80000092, 0x80000093, 0x80000095, 0x80000097, 0x80000099, 0x8000009b, 0x8000009c, 0x8000009e, 0x8000009f, 0x800000a0, 0x800000a1, 0x800000a2, 0x800000a3, 0x800000a4, 0x800000a6, 0x800000a7, 0x800000a8, 0x800000a9, 0x800000ad, 0x800000ae, 0x800000af, 0x800000b0, 0x800000b2, 0x800000b3, 0x800000b4, 0x800000b5, 0x800000b6, 0x800000b7, 0x800000b9, 0x800000ba, 0x800000bb, 0x800000bc, 0x800000bd, 0x800000be, 0x800000bf, 0x800000c3); whether all mapped PRX modules exist as separate runtime files or are embedded in system daemons on retail consoles is unverified - Dual IDs for `libSceNgs2` (0x0000000b user / 0x80000090 internal) and `libSceHdrScopes` (0x00000109 user / 0x800000bd internal) reflect distinct public and internal sysmodule identifiers defined in PS5 firmware SDKs +- [sceSysmoduleIsCalledFromSysModule, sceSysmoduleIsCameraPreloaded, sceSysmoduleLoadModuleByNameInternal, sceSysmoduleMapLibcForLibkernel, sceSysmodulePreloadModuleForLibkernel and sceSysmoduleUnloadModuleByNameInternal](../../../core/libs/prx/libSceSysmodule/Export.cpp) (libSceSysmodule) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSysmodule.c). `sceSysmoduleLoadModuleByNameInternal` takes `(uint8_t* fname, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5)` from [SharpProspero's Sysmodule bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Sysmodule/Sysmodule.cs#L149-L151), not measured on a console. The other exports had no credible prototypes and are declared without parameters solely to resolve known imports; they make no ABI claim. ### Functional diff --git a/docs/dev/debt/libSceSystemGesture.md b/docs/dev/debt/libSceSystemGesture.md index 75be9ed8c8..e0bc2e09c2 100644 --- a/docs/dev/debt/libSceSystemGesture.md +++ b/docs/dev/debt/libSceSystemGesture.md @@ -2,8 +2,9 @@ ### Silent stubs -- [System gesture recognizers](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - `sceSystemGestureGetTouchEvents`, `GetTouchEventsCount`, `GetTouchEventByIndex` and the recognizer append, remove, reset and update calls report that no recognizer ever matches, so a title receives primitive touch events but no tap, drag, hold, pinch, rotation or flick event. Handle and pointer arguments are still checked across all query entry points. Error codes follow the 0x80D1 values main already uses in this file; KytyPS5 uses 0x8089 (INVALID_ARGUMENT 0x80890001, INVALID_HANDLE 0x80890006), and which one the console returns is not verified. `sceSystemGestureGetTouchRecognizerInformation` returns the gesture type and rectangle given to `sceSystemGestureCreateTouchRecognizer` or `UpdateTouchRecognizerRectangle`, stored in the title's recognizer buffer in a layout private to this file, and always reports `updated_time` 0 because no recognizer is ever updated with input. It throws for a recognizer that was not created by `sceSystemGestureCreateTouchRecognizer` or has no rectangle, since the console's result in those cases is not known +- [System gesture recognizers](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - `sceSystemGestureGetTouchEvents`, `GetTouchEventsCount`, `GetTouchEventByIndex`, `GetTouchEventByEventID` and the recognizer append, remove, reset and update calls report that no recognizer ever matches, so a title receives primitive touch events but no tap, drag, hold, pinch, rotation or flick event. Handle and pointer arguments are still checked across all query entry points. Error codes follow the 0x80D1 values main already uses in this file; KytyPS5 uses 0x8089 (INVALID_ARGUMENT 0x80890001, INVALID_HANDLE 0x80890006), and which one the console returns is not verified. `sceSystemGestureGetTouchRecognizerInformation` returns the gesture type and rectangle given to `sceSystemGestureCreateTouchRecognizer` or `UpdateTouchRecognizerRectangle`, stored in the title's recognizer buffer in a layout private to this file, and always reports `updated_time` 0 because no recognizer is ever updated with input. It throws for a recognizer that was not created by `sceSystemGestureCreateTouchRecognizer` or has no rectangle, since the console's result in those cases is not known ### Unknown function info - [Primitive touch events](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - `sceSystemGestureUpdatePrimitiveTouchRecognizer` reads the pad reports the title passes in, `{int32 padHandle, int32 reportNumber, const ScePadData* padDataBuffer, 8 reserved bytes}`, as [fpPS4](https://github.com/red-prig/fpPS4/blob/585d06fa7c04698c41272b95a696a14c4180e13e/src/ps4_libscesystemgesture.pas) records `SceSystemGestureTouchPadData`; `padHandle` is not checked and a report of a pad other than the one the title opened is not told apart. The touch states 0 inactive, 1 begin, 2 active and 3 end are the Vita ones in [RPCS3](https://github.com/RPCS3/rpcs3/blob/38c444cfa1dbe152daa63b1703c12172078cd056/rpcs3/Emu/PSP2/Modules/sceSystemGesture.h), whose recognizer, rectangle and primitive event layouts are the ones main already uses, and no public source states the PS5 values. A primitive keeps the touch id the pad reports, its positions are the pad's touchpad coordinates unscaled, `delta_time` and `elapsed_time` come from the reports' own `timestamp`, and `is_updated` is 1 for a primitive whose state or position changed in the update; the console's units, scaling and `is_updated` rule are unverified. An ended touch stays readable as one `end` event until the next report sampled drops it. A null `pInputData`, a null `padDataBuffer` or a `reportNumber` of 0 or less leaves the events unchanged and returns success, an event buffer shorter than the event count reports as many events as it holds, and `sceSystemGestureGetPrimitiveTouchEventByPrimitiveID` throws for an id that no tracked touch has; what the console answers in those cases is not known +- [sceSystemGestureDebugGetVersion](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSystemGesture.c), but no credible prototype was found. It is declared without parameters solely to resolve known imports; this is not an ABI claim diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 0786239425..9339410b2f 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -16,6 +16,8 @@ - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format, time format and enter button stay fixed - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the time zone (parameter 4) and summer time (parameter 5) follow the host's local time zone at the moment of the call. Titles compute the local time as UTC plus the time zone in minutes plus 60 minutes when summer time is 1 ([Apollo](https://github.com/bucanero/apollo-ps4/blob/252a9d5a187c463994263009851a179f4f910c49/source/exec_cmd.c#L70-L92), [GoldHEN's frame logger](https://github.com/GoldHEN/GoldHEN_Plugins_Repository/blob/6c3a06f53d9caebadfb0aeed9bb94119012010a7/plugin_src/frame_logger/source/main.c#L60-L82), and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/413d2e3518fb435f40971d238fe0ecae7ee551da/src/SharpProspero/Platform/SystemParameters.cs#L75-L79) calls them the offset from UTC in minutes and whether summer time is in effect), so the time zone is the host's current offset east of UTC less the summer time shift while summer time is in effect: 60 minutes on Linux, and on Windows the C runtime's own summer time bias (`_get_dstbias`), which is also what shifted its local time. For a host zone whose summer time is not one hour, Linux keeps the sum right and the standard offset wrong, and Windows the opposite; how the console reports such a zone is unknown. libkernel's `sceKernelConvertUtcToLocaltime`, `sceKernelConvertLocaltimeToUtc` and `sceKernelGettimezone` still use UTC +- [libSceSystemService firmware 3.20 pending exports](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) - 477 symbol names are present in [the PS5 3.20 libSceSystemService export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSystemService.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`. + ### Functional - [sceSystemServiceLoadExec](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - besides `"exit"`, it starts the title's own `/app0/eboot.bin` or another executable under `/app0` that the relinker converted with `--executable`, which is looked up beside the running one (`/app0/picker/picker.bin` as `picker.picker.bin` with the running executable's extension). The main executable is the one `APS5_EBOOT` names, which the first call sets to its own path. After the guest shutdown the old process ends without running guest exit handlers or host destructors; as during `exit`, a guest thread that calls a library the shutdown has stopped waits for the end instead of unwinding. `"exit"` ends the process after the host shutdown without running the title's `atexit` and `__cxa_atexit` handlers, since the console's shell closes the title instead; a line the title writes just before through its own thread, such as a forwarded standard error line, may not be printed. [VLC for PS5](https://github.com/ZiZc3/VLC/blob/86bb7d474a303f62bb3c2fc12f728db4846085b8/src/platform_ps5.c#L565-L571) (PPSA85300) relaunches itself with no arguments; RetroArch (PPSA99169) starts its frontend picker with `{"", NULL}`, and the picker starts `eboot.bin` with `{"--ps5-mode=retroarch", NULL}` or `/app0/es-de/es-de.bin` ([picker.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/frontends/picker/picker.cpp#L444-L446)), so the list holds only arguments. A list with a first entry becomes the whole guest `argv`, with no program name in front ([LoadExec.cpp](../../../core/libs/prx/libSceSystemService/LoadExec.cpp)): a [PS5 RetroArch console transcript](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/evidence/loadexec-second-image/relaunch-test.jsonl) records a restart with one argument entering with `argc=1`, `argv[0]="--ps5-relaunch=1"`, and a launch from the home screen with `argc=1`, `argv[0]=""` ([relaunch_ps5.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/src/relaunch_ps5.cpp#L161-L164)). For such a list Linux replaces the process with `fexecve`, and macOS with `posix_spawn` and `POSIX_SPAWN_SETEXEC`: the PID and the standard streams stay, the other descriptors are closed and the working directory becomes the executable's. Windows starts a suspended process that inherits only the standard streams and a handle of the old process, and its entry stub waits for the old process to end before it loads the title's libraries, so a shell or script that started the title sees it end. A null list or one without a first entry keeps the earlier launch: the new executable gets only its own path as its command line (`CreateProcessW` on Windows, `execv` on Linux and macOS), so its `argv[0]` is the host path, and the old process ends without waiting for it. What the console passes for such a list is not known, nor are its argument limits and error codes. Paths outside `/app0` and unconverted executables throw diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index 95e0067125..bbb48c53ce 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -2,11 +2,13 @@ ### Unknown function info +- [Bound condition variables](../../../core/libs/prx/libSceUlt/Export.cpp) use the creation-time mutex, inferred from PPSA28061 callers of `_sceUltConditionVariableCreate`; the wait caller reloads only the condition argument. Error precedence, busy/state errors, FIFO signal selection and unsupported recursive waits have not been measured on PS5 hardware. Non-null options and waits at recursion depth above one throw. A live condition prevents destroying its bound mutex. Finalization follows the existing host mutex lifecycle: it invalidates conditions and wakes queued or reacquiring waiters with a state error. Scheduling priority and build-version validation are not implemented. + - [libSceUlt ulthreads](../../../core/libs/prx/libSceUlt/Export.cpp) - `_sceUltUlthreadRuntimeOptParamInitialize`, `_sceUltUlthreadRuntimeCreate` and `_sceUltUlthreadCreate` are assumed to take the same arguments as the existing exports without the underscore, including the trailing build version. `sceUltUlthreadTryJoin` is assumed to return `ULT_ERROR_BUSY` while the ulthread runs, and `sceUltUlthreadRuntimeDestroy` to return `ULT_ERROR_BUSY` while an ulthread created on the runtime is not joined. Neither error has been confirmed on PS5 hardware - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. - [sceUltQueueCreate, sceUltQueueDataResourcePoolCreate](../../../core/libs/prx/libSceUlt/Export.cpp) - a non-null option parameter is accepted and ignored, as the other Ult create calls already do. [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/00d087b2323bf377fd39b7163a256977383cb232/include/orbis/Ult.h) declares `sceUltQueueOptParamInitialize` and `sceUltQueueDataResourcePoolOptParamInitialize` without their structures; the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.h#L25-L78) defines both as a 16-byte reserved header followed by reserved fields, 128 bytes, with no settings, and [fpPS4](https://github.com/red-prig/fpPS4/blob/5ffbdbbd76b4da6e4b8721743ea491f494b28913/src/ps4_libsceult.pas#L142-L172) keeps them as 128 opaque bytes. That the PS5 structures carry no settings either is assumed from these, not verified - [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured - +- [sceUltConditionVariableSignalAll, sceUltGetConditionVariableInfo, sceUltGetMutexInfo, sceUltGetQueueDataResourcePoolInfo, sceUltGetQueueInfo, sceUltGetReaderWriterLockInfo, sceUltGetSemaphoreInfo, sceUltGetUlthreadInfo, sceUltGetUlthreadRuntimeInfo, sceUltGetWaitingQueueResourcePoolInfo, sceUltMutexTryLock, sceUltReaderWriterLockDestroy, sceUltReaderWriterLockLockRead, sceUltReaderWriterLockLockWrite, sceUltReaderWriterLockTryLockRead, sceUltReaderWriterLockTryLockWrite, sceUltReaderWriterLockUnlockRead, sceUltReaderWriterLockUnlockWrite, sceUltUlthreadExit, sceUltUlthreadGetSelf, sceUltUlthreadYield, sceUltAbstractThreadFactoryCreateThread, sceUltAbstractThreadFactoryGetNumMaxThreads, sceUltAbstractThreadFactoryJoinThread, sceUltKernelThreadFactoryFinalize, sceUltKernelThreadFactoryGetFactoryPointer, sceUltKernelThreadFactoryInitialize, sceUltModuleFinalize, sceUltModuleInitialize, sceUltUlthreadFactoryFinalize, sceUltUlthreadFactoryGetFactoryPointer, sceUltUlthreadFactoryGetWorkAreaSize, sceUltUlthreadFactoryInitialize, sceUltConditionVariableCreate, sceUltConditionVariableOptParamInitialize, sceUltQueueDataResourcePoolOptParamInitialize, sceUltQueueOptParamInitialize, sceUltReaderWriterLockCreate, sceUltReaderWriterLockOptParamInitialize, sceUltSemaphoreOptParamInitialize, sceUltUlthreadOptParamInitialize, sceUltWaitingQueueResourcePoolOptParamInitialize](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) - throwing stubs declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware ### Functional - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; its existing size formula and the 512-byte object layouts remain unverified. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. diff --git a/docs/dev/debt/libSceUsbStorage.md b/docs/dev/debt/libSceUsbStorage.md new file mode 100644 index 0000000000..e1bc7fc86b --- /dev/null +++ b/docs/dev/debt/libSceUsbStorage.md @@ -0,0 +1,6 @@ +# libSceUsbStorage technical debt + +### Unknown function info + +- [sceUsbStorageClearFakeWriteProtection, sceUsbStorageSetFakeMapLockForDebug, sceUsbStorageSetFakeWriteProtection](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceUsbStorage.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. +- [sceUsbStorageRequestMapWSB](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - its prototype follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L245-L246), not measured on a console; the throwing declaration only resolves imports. diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index 7383f1640a..8554ed23db 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. @@ -28,6 +29,7 @@ ### Functional - [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 - [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. diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index 5983d70f39..c7ee43b953 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -9,3 +9,5 @@ - [sceVoiceResetPort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - the `(uint32_t portId)` signature is the PS Vita one in [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L503-L514) and the PS3 `cellVoiceResetPort` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/be69c054eeac57e2e671697a463473d37fe5a7b2/rpcs3/Emu/Cell/Modules/cellVoice.cpp#L493-L510); that the PS5 library takes the same argument is assumed. A reset clears a port's buffered data, and ports here buffer nothing (`sceVoiceReadFromOPort` returns 0 bytes and `sceVoiceWriteToIPort` throws), so it only checks that the library is initialized and the port exists, as RPCS3 does, and keeps the port's connections, volume and mute flag. An uninitialized library or an unknown port throws like the other port calls; the console's error codes are not measured. PPSA23566 imports it - [sceVoiceGetResourceInfo](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - the `(VoiceResourceInfo* info)` signature and port limits (7 voice inputs, 7 voice outputs, 1 device input, 1 device output, 64 total ports) are the PS Vita ones in [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L150-L157); that the PS5 library uses the same layout and limits is assumed. An uninitialized library or a null output pointer throws like the other Voice calls; console limits and error codes are not measured. PPSA23566 imports it - [sceVoiceEnableChat, sceVoiceDisableChat](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) take no arguments and return 0, following [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L460-L464) and the `sceAudioOut2EnableChat` model. The host mixes no chat audio, so the calls only record whether chat audio is enabled and fail when the library is not initialized. Console error codes and chat session requirements are unverified. PPSA23566 imports them +- [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 +- [sceVoicePausePort, sceVoiceResumePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5: the `(uint32_t portId)` signature follows [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L516-L538) and the PS3 calls in [RPCS3](https://github.com/RPCS3/rpcs3/blob/be69c054eeac57e2e671697a463473d37fe5a7b2/rpcs3/Emu/Cell/Modules/cellVoice.cpp#L443-L529), and the console's error codes and its result for pausing a paused port or resuming a running one are unknown (0 here, as in RPCS3). Both only check that the library is initialized and the port exists: VitaSDK documents that a paused port keeps its data and reported state, and nothing here processes port data. PPSA24191 imports them diff --git a/docs/dev/debt/libSceWebBrowserDialog.md b/docs/dev/debt/libSceWebBrowserDialog.md index e5ecd3a525..3d44927711 100644 --- a/docs/dev/debt/libSceWebBrowserDialog.md +++ b/docs/dev/debt/libSceWebBrowserDialog.md @@ -4,3 +4,7 @@ - [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 real export takes a second argument, the allow-list of up to twenty addresses (SharpProspero `docs/dialogs.md`); it is not read here, so no address filtering is modelled, and the exact validation order and codes with a null second argument are unverified. - [libSceWebBrowserDialog](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) - `sceWebBrowserDialogClose` transitions an initialized dialog directly to `COMMON_DIALOG_STATUS_FINISHED` even when called before `Open`, following the Signin / NetCtlAp / PlayerSelection convention; this behavior is unverified against real hardware. + +### 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 d9b2e588a8..931d7d1746 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -2,6 +2,8 @@ ### 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. + - [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. Implemented only on Linux; 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 they belong to one native module. 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. @@ -10,7 +12,7 @@ - [_Mtx_trylock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `k6pGNMwJB08`) - returns busy for a non-recursive mutex already owned by the caller, assumed to match `_Mtx_lock`'s ownership handling; exact console behavior is unverified. - [nearbyintf](../../../core/libs/prx/libc/src/Math.cpp) (libc, NID `c+4r-T-tEIc`) uses the host rounding mode, not the console's; console rounding mode state remains unverified. - [__std_find_trivial_unsized_N](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc) - matches the `libc.prx` bundled with PPSA08427 (disassembly, and that binary run side by side on random inputs): when nothing matches between `first` and the end of its 32-byte-aligned block, the console rescans that whole block without masking and so can return a match up to 31 bytes before `first` -- [_Thrd_sleep](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `jfRI3snge3o`) follows the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`xtime.c`) and C11 Annex K, without console verification. It reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, retrying on `EINTR`, so it is a cancellation point; a null xtime or one whose normalization overflows throws. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. +- [_Thrd_sleep](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `jfRI3snge3o`) follows the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`xtime.c`) and C11 Annex K, without console verification. It reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep` on POSIX, retrying on `EINTR`, or high-resolution waitable timers on Windows; a null xtime or one whose normalization overflows throws. Guest thread cancellation is not integrated with this wait. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. - [_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. This console ABI is assumed, as for `fegetenv`, not measured on hardware. Null flag pointers throw instead of faulting. @@ -61,13 +63,15 @@ - [getc_unlocked, putc_unlocked, getchar_unlocked, putchar_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. - [fgetws, getwc, getwchar, putwc, putwchar](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - wide stream I/O encodes and decodes wide characters as UTF-8 on the underlying byte stream; `fgetws` reads characters into the destination buffer until newline or EOF and null-terminates, returning null on stream error. Console wide stream buffering and encoding details remain unverified. - [Closed standard descriptors](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - on Linux and macOS, a descriptor 0, 1 or 2 that is closed when libc loads is opened on `/dev/null` (read-only for 0, write-only for 1 and 2) by a static initializer of libc, which runs before the libraries that depend on libc initialize and before the title starts. Within libc this relies on no static initializer opening a descriptor before it, which `guest_closed_standard_descriptors` checks. glibc does the same for set-user-ID programs ([`check_fds.c`](https://sourceware.org/git/?p=glibc.git;a=blob;f=csu/check_fds.c)). Without it the first descriptor a library opens, the write watch's `userfaultfd` for one, becomes the guest's standard input or output. What the console gives a title as its standard descriptors is not measured, and on Windows a process started without standard handles is not handled. +- [Guest path names](../../../core/libs/prx/libc/src/General.cpp) (libc) are checked against FreeBSD 11.4's `NAME_MAX` (255 bytes per component) and `MAXPATHLEN` (1024 bytes with the terminating NUL) before they are resolved, and a longer name fails with `ENAMETOOLONG`, as `namei` does. The host's own limit still applies below that: on Windows a path whose host form exceeds `MAX_PATH` fails with `ENOENT`. A backslash counts as a path separator for the component length, because the guest path resolver turns it into `/` before the host call, while FreeBSD treats it as an ordinary name character, so a 301-byte name that contains one is accepted here. `sceNgs2ParseWaveformFile` resolves its path without a try block, so a long component ends in an uncaught `filesystem_error` there where a missing file ended in a `runtime_error` (both end the title). A path through a regular file (`file/x`, `file/`) is not always `ENOTDIR` on the calls that resolve it. None of this was measured on a console. - [ftrylockfile](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it - [fgetln](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) throws: in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5FE80) the export is a `hlt` instruction, which faults in user mode and ends the process - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. -- [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `NAN(n-char-sequence)` converts to the quiet NaN that `nan` and `nanf` return, negated after a minus sign, and ignores the sequence as they ignore their tag. This is inferred from C17 7.12.11.2p2, which makes `nan("n")` equivalent to `strtod("NAN(n)", NULL)`, and not observed on the console. No console `nanl` was observed, so `strtold` returns the x87 default quiet NaN (exponent `0x7fff`, significand `0xc000000000000000`) +- [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console +- [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. - [jrand48 and mrand48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) return the high 32 bits of the new 48-bit state zero-extended to 64 bits, as the console's `libSceLibcInternal.sprx` does when called from a homebrew payload process on a PS5 (firmware 12.70): a result with bit 31 set comes back between 0x80000000 and 0xFFFFFFFF, as FreeBSD 11.0's [`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/jrand48.c#L23) and [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/mrand48.c#L25) return it; FreeBSD 11.1 and later cast it to `int32_t` ([`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/jrand48.c#L26), [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/mrand48.c#L28)), which makes it negative. `lrand48` and `nrand48` matched FreeBSD there. Whether a title's own libc does the same is unverified - [seed48 and lcong48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x6F460 and 0x6F420): the multiplier and addend are shared by the whole `rand48` family, including `erand48`, `nrand48` and `jrand48` on a caller's seed; `lcong48` sets them with the seed, and `srand48` and `seed48` restore 0x5DEECE66D and 0xB. Unlike FreeBSD, `seed48` returns a static copy of the new seed rather than the previous one. A null argument, which the console dereferences, throws @@ -76,6 +80,7 @@ - [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 - [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 console's `y0f`, `y1f` and `ynf` raise none either, unlike [ours](../../../core/libs/prx/libc/src/Math.cpp), which raise `FE_DIVBYZERO` at ±0. 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 - [mergesort](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) checks its arguments like the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x762D0): an element size under 4 sets `errno` to `EINVAL` (22) and returns -1, an empty array returns 0, and a failed `malloc` of the count times the size plus 8 bytes returns -1. The sort is a stable bottom-up merge, not FreeBSD's natural merge with its insertion sort for short runs, so the comparator is called in a different order and number of times; the result is the same for a consistent comparator. A null base or comparator and a size product that overflows, which the console does not check, throw - On macOS the [write watch](../../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: @@ -91,7 +96,7 @@ - The [`_Unwind_Context`](../../../core/libs/prx/libc/include/exceptions/Unwind.hpp) that `_Unwind_Backtrace` passes to its callback starts with a table laid out like LLVM libunwind's `AbstractUnwindCursor` virtual table, because homebrew built with the ps5-payload-dev SDK links libunwind statically and reads the frames with its own `_Unwind_GetIP` (`__unw_get_reg`), which calls through that table. Only register reads and writes (`validReg`, `getReg`, `setReg`, with `UNW_REG_IP` and `UNW_REG_SP`), `validFloatReg` (always false) and `isSignalFrame` are answered; the other slots throw. The layout follows libunwind's `UnwindCursor.hpp`, not the console's own `libSceLibcInternal`, which is not checked - The Windows libc [exception unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) steps through frames of modules without the project's `.ehmeta` and `.ehfram` tables (system DLLs, and WinLibs GCC 15.2's `libstdc++-6.dll` and `libwinpthread-1.dll`, whose `.eh_frame` sections are empty) with their PE unwind data through `RtlVirtualUnwind`, but does not run Windows exception or termination handlers: it stops at a frame whose unwind data has one, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), and the exception is then reported as unhandled - The 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 the host's IPv4 and IPv6 addresses in FreeBSD's `ifaddrs` and sockaddr layouts. FreeBSD's per-interface AF_LINK entries (MAC, index, MTU) and their `if_data` are not produced, so interfaces without an address are omitted. Names are the host's (Linux `lo`, `eth0`; Windows NDIS names such as `ethernet_32769`, or `if` when one does not fit 15 bytes), not the console's. A Linux point-to-point address without a peer loses `IFF_POINTOPOINT`, and an IPv4 broadcast address the host omits is computed from the netmask. On Windows the flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST`, derived from the adapter: PPP and tunnel adapters get neither `IFF_BROADCAST` nor `IFF_POINTOPOINT` because their peer address is not reported, and an address whose prefix length the host reports as unspecified gets a host mask. +- [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists host interfaces and IPv4/IPv6 addresses in FreeBSD's layouts. Windows omits per-interface AF_LINK entries and interfaces without an address; its flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST` (PPP/tunnel adapters omit broadcast and point-to-point, and an unspecified prefix length gets a host mask). On Linux, `AF_LINK` `if_data` maps non-Ethernet/loopback/PPP types to `IFT_OTHER` (1) and leaves `ifi_omcasts`, `ifi_noproto`, `ifi_hwassist`, `ifi_metric`, `ifi_epoch`, `ifi_lastchange`, unknown baud rates and address-entry `ifa_data` as 0/null; a point-to-point address without a peer loses `IFF_POINTOPOINT`, and an omitted IPv4 broadcast is derived from the netmask. Interface names are the host's (`if` on Windows above 15 bytes). - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. - [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) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x7D480 to 0x7D920), which converts to and from UTF-8 whatever the locale, with the original sequences of up to six bytes and no check for surrogates or for values above 0x10FFFF when encoding. The conversion state keeps the value in its first eight bytes and the count of bytes still expected (or 0x10 for a pending low surrogate, 1 for a stored high surrogate) as 16 bits at offset 0xA; a null state, which the console dereferences, throws. As on the console, `mbrtoc16` does not store the complete code point when it returns a high surrogate, so the low surrogate of the next call is built from the value the state last held, which is the partial value of an earlier incomplete call or whatever the caller initialised it to, `c16rtomb` returns -1 without setting `errno` when a high surrogate is not followed by a low one, and `mbrtoc16` and `mbrtoc32` report `EILSEQ` as 86. Measured on a PS5 (firmware 13.60) from a homebrew payload, the system `libSceLibcInternal`'s default locale also converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen` (`wctomb` of U+263A gives `E2 98 BA`, `mblen` of a lone 0xE9 gives -1); this libc keeps its single-byte C locale, so [wctomb and mblen](../../../core/libs/prx/libc/src/Multibyte.cpp), which call `wcrtomb` and `mbtowc` here as they call the same internal routines on the console, share that difference. Titles bundle their own `libc.prx`, whose locale was not measured diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index e6bdd3c6fc..55dfa98df6 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -4,7 +4,7 @@ - [sceKernelGetSocPowerConsumption](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - returns `EINVAL` and writes nothing, as `sceKernelGetSocSensorTemperature` does when the host has no sensor: the host's package power is not read (Linux RAPL `energy_uj` is readable by root only, and Windows has no general source). The prototype is not documented. PS5LM Plus (PPSA99581) passes a zeroed 128-byte buffer and, on success, reads its first 32-bit word as milliwatts, accepting 1000 to 350000; on failure it shows 0 W - [scePthreadMutexattrSetprotocol](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) - accepts 0 to 2 as a PS5 (firmware 13.60) does and returns `EINVAL` otherwise, but the protocol is not stored or applied: priority inheritance and protection are not modelled. On the console a null or uninitialized mutex attribute returns `EINVAL` from the SCE attribute calls, `scePthreadMutexInit` takes an uninitialized attribute as the default one, and a destroyed mutex returns `EINVAL` from `scePthreadMutexDestroy`; a null mutex pointer crashed the payload and still throws -- [pthread_mutexattr_setprotocol, pthread_mutexattr_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 +- [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 - [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) reports 0 bytes and returns 0: what the console's internal memory pool holds and how large it is are unknown. The `int (size_t*)` signature is from [xUtilty](https://github.com/Backporter/xUtilty/blob/328a856af1ef/source/SystemWrapper.cpp#L85) (PS4). PPSA99203 calls it once beside `sceKernelAvailableFlexibleMemorySize` while starting its GS renderer and never reads the result ([GS.cpp](https://github.com/Swordpdf/PS5SX2/blob/b4cf55e87c34/pcsx2/GS/GS.cpp#L233-L247)) @@ -16,7 +16,7 @@ - [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`. - [sceKernelGetSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000); the console's firmware version is not modeled. Measured on PS5 firmware 13.42: record values 0, 24, 39, 40, 41, 48 and 64 in the leading 64-bit field are accepted and preserved, the 28-byte string at offset 8 and packed version at 0x24 are filled, and bytes from 0x28 onward remain untouched. This API reports "13.590.001" / `0x13590001` in that environment, while the Prospero variant reports "13.420.000" / `0x13420000`; their values must not be assumed identical. A null pointer still returns `SCE_KERNEL_ERROR_EFAULT`, which was not measured; other firmware versions and record values remain unverified. -- [kern.rng_pseudo](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` fills the whole output buffer with bytes from the host's `std::random_device` and leaves the length unchanged; a write returns -1 with `EPERM` and a call without an output buffer throws. 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, its per-call maximum and its answer to a size query are unknown +- [kern.rng_pseudo](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` draws a new block of 64 bytes from the host's `std::random_device` for each read (no new value) that passes a buffer and a length, and sizes its answers as a payload process on a PS5 (firmware 12.70) sees it: a size query reports 64; a buffer of 64 bytes or more gets 64 bytes and a length of 64 (tried up to 65536); a smaller buffer gets as many bytes as its length, which is left as given, and a null length gets none, both with -1 and `ENOMEM`; a null buffer with a null length returns 0. `errno` is kept when the call succeeds. A write returns -1 with `EPERM`, which was not tried on the console. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator is unknown, and a title process was not measured. On the console `sysctlnametomib` gives MIB {1, 50} for the name, and `sysctl` with it answers reads of 0, 1, 63, 64, 65 and 128 bytes, a size query, a null length and a null buffer with a null length as the name does; here `sysctlnametomib` maps the name to {1, 50} and `sysctl` {1, 50} answers as the name does; a write through the MIB was not measured and throws - [sysconf](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `_SC_CLK_TCK` returns 128, FreeBSD's `CLK_TCK` as its [`sysconf`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L100-L101) returns it ([`time.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/include/time.h#L48-L54)); the console's value is not checked, and no `times` reports ticks at that rate. CPython's `posix` module stops its import with "cannot read ticks_per_second" when the value is below 1, as the Python in PS5MC (PPSA99420) does. `_SC_ARG_MAX` returns 262144 (FreeBSD's `NCARGS`), `_SC_OPEN_MAX` returns the `RLIMIT_NOFILE` soft limit via `getrlimit` ([`sysconf.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L106-L115)), and `_SC_IOV_MAX` returns 1024 (FreeBSD's `UIO_MAXIOV`). Names other than `_SC_ARG_MAX`, `_SC_CLK_TCK`, `_SC_OPEN_MAX`, `_SC_PAGESIZE`, `_SC_IOV_MAX`, `_SC_NPROCESSORS_CONF`, `_SC_NPROCESSORS_ONLN` and `_SC_PHYS_PAGES` fail with `EINVAL` - [hw.realmem](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_HW`, 12} and `sysctlbyname` report the same 8-byte value as `hw.physmem` (the `_SC_PHYS_PAGES` pages of 16 KiB), read-only with `EPERM` for a write and `ENOMEM` for a short buffer, as FreeBSD's [`sysctl_hw_realmem`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_mib.c#L191-L204) does. FreeBSD reports there the memory the firmware describes, which is at least `hw.physmem`; the console's values are not known. 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; the console's value is not known and a write throws. 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 @@ -49,6 +49,7 @@ - [sceKernelGetLibkernelTextLocation](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(void** address, std::size_t* size)` signature and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L338-L348); whether the console reports that segment, the text of libkernel itself or the calling module's is unverified. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null argument throws. - [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - unmaps the range like sceKernelMunmap, as fpPS4 and prosper do; whether the console keeps the virtual range reserved after releasing its pages is unverified - [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 13.5 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) +- [sceKernelAvailableDirectMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - how the console handles invalid arguments is not measured. A negative `search_start`, a `search_end` at or below `search_start` and an alignment that overflows when rounded up to 16 KiB return `EAGAIN` (`0x80020023`); bounds that are not multiples of 16 KiB are rounded inward, the alignment is raised to a multiple of 16 KiB and need not be a power of two, and a `search_end` past the pool is cut to it. fpPS4's HLE wrapper on its trunk branch refuses all of these with `EINVAL` ([ps4_map_mm.pas](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_map_mm.pas#L558-L570)) - [Memory protection bits 0x100 and 0x200](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits - [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 @@ -74,7 +75,7 @@ - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown that is below `GuestSockets::FirstDescriptor` is polled on the host instead of throwing (on Windows only a pipe, as above); an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. A descriptor that stops being a socket, a kqueue or a pipe during the wait fails the call with `EBADF` when it is closed and is selected again with the rest of the timeout when its number was reused, as FreeBSD 11.4's [`kern_select`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1129-L1140) looks every descriptor up again in each pass. A close that has already removed the descriptor from the guest's kqueue, pipe or file tables but not yet closed its host descriptor is seen by that new look-up as a descriptor of unknown kind, which throws on Windows and is polled through the host on Linux. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles - [__progname](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [environ](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - data export pointing to a fixed empty environment so titles that import it load; it is not the environment libc's `getenv` reads, so it lists none of those variables and `setenv`/`putenv` changes do not appear in it -- [seteuid, setegid, setuid, setreuid, setregid, setgroups, getgroups](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid`, `sys_setreuid` and `sys_setregid`, id 0 (or -1 for an unchanged `setreuid`/`setregid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified +- [seteuid, setegid, setuid, setgid, setreuid, setregid, setgroups, getgroups](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid`, `sys_setgid`, `sys_setreuid` and `sys_setregid`, id 0 (or -1 for an unchanged `setreuid`/`setregid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified - [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) (libkernel) takes `(void* info, size_t size)` as in the [Orbital type database](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json); the 16-byte, 8-byte-aligned block follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which places coredump reads inside the handler. The handler is never invoked here, so a well-formed request returns `NOT_IN_COREDUMP_HANDLER` without reading or writing the block. Its value `0x81180003` follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/coredump/coredump_error.h) and the NHL 27 (PPSA34063) call site reported in [#4275](https://github.com/boykopovar/AnyPS5/discussions/4275), not a PS5 measurement. The console validation order is unverified: null, unaligned or wrapping buffers and sizes other than 16 throw instead of guessing an argument error. Information from an active coredump handler remains unsupported @@ -120,16 +121,18 @@ - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Directory timestamp updates resolve the current path of the opened handle. Filesystems with coarser timestamp resolution cannot retain every guest microsecond - [EVFILT_VNODE on macOS](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) registers `EVFILT_VNODE` on a duplicate of the descriptor in a host kqueue, whose notes have FreeBSD's values, and drops the `NOTE_LINK` that XNU adds to `NOTE_DELETE` when the watched file or directory is removed, since FreeBSD's `vop_remove_post` and `vop_rmdir_post` report `NOTE_DELETE` alone. As on Windows, a directory registered with `NOTE_EXTEND` is accepted but never receives it: XNU reports a move between directories as `NOTE_WRITE` alone, with `NOTE_LINK` for a subdirectory - [Windows file identity](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails -- [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction is reported the same way when the name left after that prefix is a drive path (`X:\...`): the print name when that name is a drive path, otherwise the substitute name. A mounted volume, whose substitute name is `\??\Volume{GUID}\`, and any other reparse point throw, as for `readlink` +- [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction is reported the same way when the name left after that prefix is a drive path (`X:\...`): the print name when that name is a drive path, otherwise the substitute name. A mounted volume, whose substitute name is `\??\Volume{GUID}\`, and any other reparse point throw, as for `readlink`. `open` with `O_NOFOLLOW` returns `EMLINK` for that junction and does not open the target - [setitimer](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `sys_setitimer` and `kern_setitimer`: `EINVAL` for timers other than `ITIMER_REAL`, `ITIMER_VIRTUAL` and `ITIMER_PROF`, and for a value, or the interval of a non-zero value, with a negative field, `tv_usec` of 1000000 or more, or more than `INT32_MAX / 2` seconds; a null value reads the timer as `getitimer` does, with `EFAULT` when the output is null too. `ITIMER_REAL` shares the timer thread of `alarm`, which clears its interval: a zero value disarms it, the previous value is returned, and each expiry re-arms it by `it_interval`. With `SIG_IGN` an expiry is dropped. With `SIGALRM` blocked it is left on the pending set of `sigtimedwait`, as `kern_psignal` does, where `sigwait`, `sigtimedwait` and `sigwaitinfo` take it; unblocking it runs a guest handler on the unblocking thread, and with the default disposition exits the process with status 142. Unblocked, the default disposition exits the process with status 142 and a handler throws, since guest `SIGALRM` handlers are never run from the timer thread. The value is not raised to `tick / 16` as `itimerfix` does, and an interval below 1 ms throws, since the console's `hz`, and so that rounding, is unknown (1 ms covers any `hz` of 63 or more). `ITIMER_VIRTUAL` and `ITIMER_PROF` throw when armed, as process CPU time is not accounted, and read and disarm as zero. PPSA99420 imports it and fails to load without it - On Linux, [direct memory](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [system](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed; returns `EFAULT` (`0x8002000e`) when passed a null status buffer pointer. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Windows directory descriptors](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout: `getdents` returns `ENAMETOOLONG` without consuming the entry. Repeated calls therefore report the same error and entries after that name cannot be reached through this descriptor; FreeBSD's `getdents` behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies. Descriptors retain the opened directory's native handle across renames. Enumeration reloads and `*at` operations resolve its current path, so a concurrent rename between resolution and the operation is not atomic. Enumeration of a removed directory and network-share path queries have not been verified +- [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. - [/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 remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a read event on a listening socket, here and in `kevent`, reports the number of completed connections waiting to be accepted in `data`, as FreeBSD 11.4's [`filt_solisten`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3384-L3392) reports `so_qlen`: on Linux a TCP listener's accept queue from `TCP_INFO` (`tcpi_unacked`, which Linux fills with the accept backlog for a listening socket), and elsewhere, or for a listener that is not TCP, 1 while a connection is waiting, as no host call reports that count. A write event reports the free space of the socket's send buffer in `data`, as FreeBSD 11.4's [`filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3358-L3382) reports `sbspace`: on Linux the host's `SO_SNDBUF` less the bytes `SIOCOUTQ` counts as queued, which include the host's per-packet overhead, so the space shrinks by somewhat more than the bytes sent. Windows has no query for that space and macOS is not wired up, so both report the whole `SO_SNDBUF`, which does not shrink as data is queued. Only guest sockets are supported; other descriptors and a low watermark above 1 throw +- [sceKernelAddReadEvent on pipes](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) is the one exception to the sockets-only rule above: it also takes the read end of a pipe, as `kevent` does through FreeBSD's `filt_piperead`, and ignores the size argument there. Host streams, other descriptors and a write event on a pipe throw - [close](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) and [setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) reach a [libSceNet](../../../core/libs/prx/libSceNet/Export.cpp) socket through hooks libSceNet registers, as `select` polls them; the other libkernel socket calls do not. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards. Guest scalar transfers, positioned transfers, vectored transfers and seeks share a position lock across duplicated descriptors; a vector holds it for the whole operation. Independently opened descriptors and other processes do not share that lock, so vector transfers over their overlapping file ranges are not coordinated. Handles that are not disk files return `ESPIPE`. A pipe or console `readv` can return a short read when no more input is available; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer @@ -139,9 +142,11 @@ - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them - [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only +- [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - the answer for a thread that has ended is not measured on the console, and [`pthread_attr_get_np`](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) still returns 0 for such a thread, reading a detached one's object after it is freed. `scePthreadAttrGet` returns `ESRCH` (`0x80020003`) for a thread that has ended, joined or not and detached or not, as FreeBSD 11.4's [`_pthread_attr_get_np`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L139-L172) does through `_thr_find_thread`; a thread counts as ended once its thread-specific data destructors have run, where FreeBSD marks it `PS_DEAD`. On Windows a host thread adopted by `scePthreadSelf` that ends without `scePthreadExit` keeps answering 0. Monster Hunter Wilds (PPSA24191) reuses a slot of its thread table only when `scePthreadAttrGet` returns `0x80020003` - [scePthreadAttrGet, pthread_attr_get_np and sceKernelIsStack](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) report the stack of a host thread the library did not create, on Windows, from one allocation granule (64 KiB) above the bottom of its reservation when everything above that is committed read-write, as in a relinked executable's main thread and its default-size host threads (0x200000 committed of 0x210000 reserved), so the guest can protect the lowest reported pages, as HotSpot does for its stack guard pages in Java 25 LAN / DNS (PPSA99869). Any other such thread, and every such thread on Linux, gets its whole host stack: on Windows `mprotect` commits its reserved lower pages before protecting them and drops the guard flag of Windows' guard pages, and on Linux it cannot change the unmapped lower pages - [sceKernelInstallExceptionHandler](../../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Linux sends the host SIGUSR1, which the library takes over, to the target thread, so the handler runs wherever the thread was interrupted, emulator code included. On the console kernel objects are system calls and libthr defers signals while a thread holds its internal locks; here a handler that needs a host lock the interrupted code holds deadlocks, and a thread that the handler keeps waiting keeps its host locks. Host waits resume after the handler. The host calls Linux does not restart end early: sleeps that do not retry return before their time, and the poll of `sceNetEpollWait` and socket calls with a send or receive timeout return `EINTR` to the guest +- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows suspends the target and redirects it to the handler. A target running host code (any module other than the executable and the `*.guest.prx` modules, so libkernel, libc and the other HLE modules count too) is resumed and sampled again until it is back in guest code, since a handler that waits there would keep the host locks that code holds; a target blocked in an ntdll wait is redirected in place, as are NtContinue and winpthreads after their retry limit. A target that stays in host code without waiting for the whole 1 s retry limit, such as a long HLE or driver call, costs the raising thread up to 1 s per raise and is then redirected in host code anyway, with the same hazard. While the target is suspended only `VirtualQuery` and a lock-free cache of image bases are consulted; an image not yet in the cache is classified with `GetModuleFileNameW` after the target is resumed. An APC queued at the first host-code sample, which an alertable guest wait would run sooner, is not implemented - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) builds the handler's `ucontext` with FreeBSD's amd64 layout (16-byte sigmask, `mcontext` at 0x40, `mc_rsp` at 0xf8); the console's layout is not verified beyond the fields PS5Util reads - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask - [pthread_sigmask](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) shares the process-wide mask of `sigprocmask` and `_sigprocmask`, which keeps only the first 32 signals; FreeBSD keeps a mask per thread @@ -173,8 +178,9 @@ - [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close` - [UDP recv/recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) and [sceNetRecv/sceNetRecvfrom](../../../core/libs/prx/libSceNet/Export.cpp) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; console behavior and existing title impact are unverified. - [recvmsg](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) loses received stream data on an output copy fault: outputs are checked after the host receive. FreeBSD 11.4 leaves unread stream data queued, though it also drops a datagram on a copy fault ([`soreceive_dgram`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L2308-L2312), [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1758-L1774)). A Linux `process_vm_readv` or `process_vm_writev` failure other than `EFAULT` throws -- [MSG_DONTWAIT](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `send`, `sendto`, `sendmsg`, `recv`, `recvfrom` and `recvmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`). Winsock has no per-call non-blocking flag, so on Windows these calls still fail with `EOPNOTSUPP` for it +- [MSG_DONTWAIT](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `send`, `sendto`, `sendmsg`, `recv`, `recvfrom` and `recvmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`). Winsock has no per-call non-blocking flag, so on Windows the call first asks `WSAPoll` with a zero timeout whether the socket is ready and fails with `EAGAIN` when it is not. The check and the transfer are two host calls: a second thread that takes the data in between can leave a `recv` blocked, and a `send` of more bytes than the socket buffer holds can still block until it has sent them, where FreeBSD's `sosend_generic` returns a partial count - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). +- [bind, connect, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) check a socket address length as FreeBSD 11.4's [`getsockaddr`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_syscalls.c) and the `sa_len` test of `tcp_usr_connect` and `in_pcbconnect` do: below 2 or an `AF_INET` address below 16 or an `AF_INET6` address below 28 bytes fails with `EINVAL`, above 255 with `ENAMETOOLONG`. An address longer than `sockaddr_in` or `sockaddr_in6` is still accepted where FreeBSD answers `EINVAL`, and an address of another family still fails with `EAFNOSUPPORT`, as the console's behaviour for these is not known. None of this was measured on a console. - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set, and the guest cannot ignore it: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number @@ -198,7 +204,7 @@ - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console - [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console - [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also duplicates the descriptor (`F_DUPFD`, `F_DUPFD_CLOEXEC`) and handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2` and an accepted socket start without it, `SOCK_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. -- [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other request throws. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests +- [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` 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 - [Regular file mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - on Linux `mmap` maps a regular file's descriptor shared or private like a shared memory object, with the same access checks; SQLite's WAL index (`-shm`) in Dusklight PPSA99305 needs it. FreeBSD 11 `mmap(2)` and shadPS4's `MemoryManager::MapFile` map regular files; the console's behaviour is not measured. Other descriptors (pipes, sockets, directories, devices) still fail with `EOPNOTSUPP`. On Windows a shared mapping is a view of a file section (`CreateFileMappingW` on the descriptor's handle, read-write when the descriptor is, otherwise read-only), so writes through the descriptor and the mapping see each other. This also maps anonymous `shm_open` objects and JIT shared memory descriptors, which are temporary files on Windows. A shared mapping may end inside the 16 KiB page that holds the end of the file, as POSIX [`mmap`](https://pubs.opengroup.org/onlinepubs/9799919799/functions/mmap.html) allows (llama.cpp in PS5LM Plus PPSA99581 maps a whole model file this way): the section keeps the file's size, so the file never grows, the view ends with the 4 KiB host page that holds the end of the file, which Windows fills with zeros past it, and the rest of the guest page is private zero pages. As POSIX requires, the bytes past the end read zero and are never written to the file; bytes written to those private pages are seen only through that mapping, not through other mappings of the file, and a file that grows later does not show its new bytes there. A shared mapping that reaches a whole page past the end of the file fails with `EOPNOTSUPP`, where POSIX maps it and delivers `SIGBUS` on access, as do `PROT_EXEC` and a write-only descriptor. A read-write alias (`GuestArenaMapAlias`) of a mapped page that holds the end of the file throws. `mprotect` above the view's protection (writing through a read-only descriptor, or adding `PROT_EXEC`) fails with `EACCES` before anything changes; FreeBSD allows `PROT_EXEC` on a readable descriptor. Shrinking the file with `ftruncate` fails with `EINVAL` while any shared mapping of it exists, even one that does not cover the removed part, because the section keeps the file's size at `mmap`; FreeBSD truncates. A read-write alias of a view (`GuestArenaMapAlias`, used when the GPU imports guest memory) of a read-only file section fails, as the alias is mapped `PAGE_READWRITE`. Views of the same file made by separate `mmap` calls are separate sections, so write tracking does not treat them as aliases of each other. A private mapping on Windows is a copy of the file read at `mmap`, which POSIX allows (later changes to the file need not show); pages wholly past the end of the file fail with `EOPNOTSUPP` instead of faulting on access, and a write-only descriptor fails with `EOPNOTSUPP`. - [sendfile](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory, and a file descriptor as the socket fails with `EBADF`, as for `send`, where FreeBSD answers `ENOTSOCK`. `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. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. 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 @@ -214,8 +220,10 @@ - [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 8192 entries on each side and counts them as a PS5 (firmware 13.60) counted them for a payload process: one cpu entry for each 2 MiB granule that holds a committed mapping, whatever its protection (none included), and one gpu entry for each 1 GiB region that holds a GPU-accessible mapping, plus one for each 2 MiB granule of such a mapping that is not a whole granule of direct memory at a 2 MiB-aligned physical address. A granule or region shared by several mappings counts once, and reservations do not count. On the console the first GPU mapping of a process also takes three gpu entries and one cpu entry that stay after it is unmapped, which is not modelled, and only the ranges whose protection is recorded here are counted. A null pointer returns `SCE_KERNEL_ERROR_EFAULT`, where the console faults. Resonance: A Plague Tale Legacy (PPSA30601) calls it at start-up (#4067) - [clock_settime](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) validates like FreeBSD 11.4's [`kern_clock_settime`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_time.c#L396-L412) for a root caller: `EFAULT` for a null time, `EINVAL` for any clock but `CLOCK_REALTIME`, a negative second count or a nanosecond count outside 0 to 999999999. A valid `CLOCK_REALTIME` request throws, naming the requested time, since the host clock is never set and no guest clock offset is kept. FreeBSD checks `PRIV_CLOCK_SETTIME` first, which a jailed process fails with `EPERM`; whether a title holds it on the console is unverified. PS5MC (PPSA99420) imports it; no call site is known - [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 - [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `rtprio_thread` follows FreeBSD 11.4's `rtprio_thread(2)`. Every guest thread here runs under `SCHED_FIFO` or `SCHED_RR` (priority 256-767), whose rtprio class is real-time; the console's mapping from that priority to the rtprio `prio` range (0-31) is unmeasured, so `RTP_LOOKUP` and `RTP_SET` throw for such a thread rather than report an impossible priority. A thread that is not real-time reports `RTP_PRIO_NORMAL` (3) with 0, and `RTP_SET` accepts only that value. Another function fails with `EINVAL`, a null `rtprio` with `EFAULT`, a thread other than the calling one throws, and an id that is not a live guest thread with `ESRCH`. `pthread_setconcurrency` stores 0 or more and returns `EINVAL` for a negative level (the error itself, as FreeBSD's libthr does, with errno untouched); `pthread_getconcurrency` returns the level, which is a hint and changes nothing about the host's concurrency. Morrowind (`mshivam019/morrowind-ps5`) imports all three at start-up - [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 - [param.sfo](../../../core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp) (libkernel) is read when `param.json` is absent, for `TITLE`, `TITLE_ID`, `DOWNLOAD_DATA_SIZE` and `USER_DEFINED_PARAM_1` to `_4`, and mounts `/data` and `/download0` like `param.json`; the localized `TITLE_00` to `TITLE_30` are not selected by the console language. +- [sysctl MIBs for kern.smp.maxcpus, kern.stackprot, kern.sched.name and kern.timecounter.hardware](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - on a PS5 (firmware 12.70), a payload process resolves these names to {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}; `sysctl` returns -1 with `ENOENT` for reads and size queries by those exact MIBs, leaving the buffer and length unchanged, including when either or both output pointers are null. Writes were not measured and throw. `sysctlnametomib` still throws for these four names; #7541 implements the mappings for nine of the other names. diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 384fbd0d1b..8719280930 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -12,7 +12,7 @@ - Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) by default, not only with `--windows-fixed-base`: on the console the executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Relocations are kept, so if `0x140000000` is taken Windows relocates the image, possibly above that window. Verified only on Windows 10. - [SDK memory-management syscall traps](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) under `--trap-sdk-syscall-stubs` recognize three exact instruction sequences from [ps5-payload-dev SDK libc/mman.c](https://github.com/ps5-payload-dev/sdk/blob/12cf3dda40/libc/mman.c), observed in Nuvio Maxd 0.5.15 (PPSA99288): syscall 74 and the two syscall 477 paths, with their argument preparation and carry-flag error handling. Matching requires a decoded instruction boundary and the complete sequence inside the scanned code range. Other SDK/compiler output remains rejected. The matched syscall becomes `UD2`; this does not implement mmap or mprotect and a call still faults. - The [instruction decoder](../../../core/relinker/codegen/src/x86/X64InstructionDecoder.cpp) reads a 16-bit displacement for `66 E8`, `66 E9` and `66 0F 80`-`8F` without REX.W, as AMD processors, the console included, do (binutils `objdump -M amd64`; REX.W selects the 32-bit form). AMD also truncates the target of every near relative branch with that prefix, rel8, LOOP and JRCXZ forms included, to 16 bits. Instruction metadata marks these branches as truncated and reports no branch target for them, and stubs refuse to move them. Compilers do not emit these forms. -- `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. +- `--to-intel` lowers MCOMMIT to STC plus NOP padding and RDPRU through a runtime-checked stub in the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp): AMD APM Vol. 3 defines RDPRU ECX 0 as MPERF and ECX 1 as APERF, neither readable on Intel hosts, so both selectors return the host TSC as a disclosed monotonic-counter approximation (their ratio always reads 1.0) while other selectors yield zero EDX:EAX. STC asserts CF=1 because under x86-TSO prior stores are already ordered and guest memory is host-DRAM-backed, leaving the commit-success case as the only observable one. Non-zero RDPRU selectors, the TSC-for-MPERF/APERF substitution, and any console path reporting MCOMMIT CF=0 are unverified. - [Unscoped imports on Windows](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) - an import without a module that no searched library exports under its name is searched again under the NID of that name, because homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) imports by symbol name (Quake, from the websrv homebrew launcher; Mari0, #6239) while the libraries in `libs/` export NIDs. A dependency named with `.sprx`, `.suprx` or `.debug_prx` loads the `.prx` library of the same stem from `libs/`, and `libkernel_web` also loads `libkernel.prx`, as `libSceLibcInternal` already loads `libc.prx`: AnyPS5's `libkernel_web.prx` exports only two functions, where the console's `libkernel_web.sprx` is a libkernel variant. A name that matches a library's export as written still binds to that export first. Linux and macOS resolve these imports through the host loader by name and are unchanged. - The length-changing path of the [instruction rewriter](../../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. - The [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp) takes the branch targets that keep an instruction in place from a linear scan of the code segment, so data in that segment decodes as branches too: OpenSSL's x86-64 assembly keeps strings such as "SHA1 block transform for x86_64, CRYPTOGAMS by " in its code, and in SuperTuxKart (PPSA69421) and Mortalfin (PPSA99141) the `jae` read from "sf" points into the VEX prefix of a 256-bit store. A direct branch target inside a scanned instruction counts only where every byte before it is a legacy prefix or REX, which a real branch can skip (a `lock` prefix, for example); a target in a VEX prefix, opcode or operand is ignored, as a jump-table entry that starts no instruction already is. A real branch to such a target, overlapping instructions as hand-written code can, is not seen, and the instruction it enters can be moved. @@ -20,6 +20,7 @@ - In Windows output, a guest instruction that addresses memory through GS becomes `UD2` and NOP padding of its own length ([TLS builder](../../../core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp)): the host owns GS, and the guest GS base that FreeBSD's `sysarch(AMD64_SET_GSBASE)` would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine `ntdll`. - In executables with a `PT_SCE_DYNLIBDATA` segment, the PS4 layout, the executable segment also holds read-only data after the code (CUSA00900: 37 MiB after its last function). The [relinker pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp) therefore scans for syscalls and import call sites only up to the end of the PLT that follows the last function of the unwind table; code after the PLT without an unwind entry is not scanned. `--to-intel` and the `unused-filter=1` and `unused-filter=2` analyses are not limited this way. - The [syscall scan of `sce_module` modules](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) with a `PT_SCE_DYNLIBDATA` segment stops at the same end, or, when the PLT comes before the code, at the end of the last function of the unwind table; code after it is not scanned. +- 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. - 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. - A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)). - In macOS output, a guest instruction with a GS segment prefix, including one with no memory operand (`65 90`, `65 48 89 c8`), which does not touch GS, becomes `UD2` and NOP padding of its own length ([macOS image writer](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp)): the host owns GS, which holds the pthread TSD base the guest TLS stubs read, and the guest GS base that FreeBSD's `sysarch(AMD64_SET_GSBASE)` would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine `ntdll`. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index c2b4084d5f..2dee38e74e 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -63,7 +63,7 @@ - [Unnormalized S#s](../../../core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp) are accepted only when direct and used only by explicit-LOD `image_sample` without comparison, derivatives or `_a`, on a single-level, single-layer 1D or 2D view of mip 0, even when the image is also sampled through a normalized S#. - [Unnormalized texel offsets](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) are added to the f32 coordinates: exact on RDNA2 (gfx1035), which applies the offset before the clamp ([measured by adriadam10](https://github.com/boykopovar/AnyPS5/pull/822#issuecomment-6033192725)). - [Cube image gradients](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (shader recompiler) - `image_sample_d` and `image_sample_cd` on a cube pass their gradients to the host sample without the -1.0 that moves the face coordinates from [1, 2] to [0, 1], as Mesa's AMD lowering builds them (`src/amd/common/nir/ac_nir_lower_tex_coords.c`, `prepare_cube_coords`: face-space derivatives with no offset, coordinates offset by 1.5). Measured on RDNA2 (gfx1035) with the [hardware oracle](../HW_ORACLE.md): on a 16x16 32_FLOAT cube with five levels, point mip filter, `image_sample_d` reads the level of `round(log2(16 * max(|ddx|, |ddy|)))` for gradients of 2^(level ± 0.3) texels on each of the four components, both signs, mixed components, every face and two coordinates (1032 rows). `image_sample_cd` takes one level per quad of lanes, from the gradients of its first lane; the host sample computes it per lane. -- [Image atomics](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) on 32_SINT and 32_FLOAT surfaces act on the raw dword through an R32_UINT storage view; their float results are assumed to match those measured on 32_UINT surfaces. Other formats throw. +- [Image atomics](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) on 32_SINT and 32_FLOAT surfaces act on the raw dword through an R32_UINT storage view. On gfx1100 (RDNA3, [hardware oracle](../HW_ORACLE.md), wave32 and wave64), all 13 integer image atomics, `swap` to `dec`, return the old dword and store the raw-dword result identically on 32_UINT, 32_SINT and 32_FLOAT T#s, over 121 old/data pairs per op, among them 0x80000000, 0xffffffff, 1.0, -1.0 and NaN: `add` adds the bits and `umin` is unsigned on every format. A 16_UINT control differs, so the format is read. gfx1031 (RDNA2) gives the same rows, and its `fmin`, `fmax` and `fcmpswap`, which gfx1100 lacks, also match on the three formats ([measured by thuanquang](https://github.com/boykopovar/AnyPS5/pull/7939#issuecomment-6275099482)). The console's gfx1013 is unmeasured. Other formats throw. - [`s_memtime`](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) on RADV with a GFX10.3 to GFX11.7 GPU (RDNA 2, RDNA 3, RDNA 3.5) reads the Vulkan device clock instead of the subgroup clock, which [ACO](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/compiler/instruction_selection/aco_select_nir_intrinsics.cpp) reads from the 20-bit `SHADER_CYCLES` register there and which wraps every 2^20 cycles. The device clock is a monotonic 64-bit counter, but it counts the 100 MHz reference clock (the same one as `s_memrealtime`), not shader cycles, so `s_memtime` deltas are about 20 times smaller than on hardware. The families are matched by the name RADV reports ([`NarrowSubgroupClock`](../../../core/libs/prx/libSceAgcDriver/Execution/src/SubgroupClock.cpp)); a family added to Mesa later reads the subgroup clock until it is listed. AMD's proprietary Windows driver also exposes a 20-bit subgroup clock on device 0x1114 (Radeon 860M, measured in #1615), device 0x744c (Radeon RX 7900 XT, measured in #1632), and device 0x747e (Radeon RX 7800 XT, measured in #1632); other AMD proprietary devices remain unverified. - [`s_memtime` and `s_memrealtime`](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) on llvmpipe read only the low 32 bits of the host's nanosecond clock at either scope: Mesa's [`emit_clock`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/gallium/auxiliary/gallivm/lp_bld_nir_soa.c) builds the high dword with a left shift by 32, so it is always 0 and both counters wrap every 2^32 ns (about 4.3 s). The [shader clock test](../../../core/libs/prx/libSceAgcDriver/tests/execution/ShaderClock.cpp) compares them modulo 2^32 there - [`ds_wrxchg2_rtn_b32`, `ds_wrxchg2st64_rtn_b32`, `ds_wrxchg2_rtn_b64` and `ds_wrxchg2st64_rtn_b64`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) are translated as two separate 32-bit or 64-bit atomic swaps per invocation. RDNA2 hardware (measured on gfx1035) performs every lane's first exchange before any lane's second one, so when lanes of the same wave or of different waves overlap addresses, the result can differ from hardware. `offset0 == offset1` throws, although on hardware it is two exchanges in order. diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 74221f95ab..79d8f05b53 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -16,7 +16,7 @@ While a title has a mouse open (`sceMouseOpen`) and the window is focused, AnyPS Game-controller bindings are enabled automatically and are not affected by keyboard or mouse overrides in this file. -Supported actions are `Cross`, `Circle`, `Triangle`, `Square`, `L1`, `R1`, `L2`, `R2`, `L3`, `R3`, `Options`, `Up`, `Right`, `Down`, `Left`, `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown`, `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown`, `TouchLeft`, `TouchRight`, `StickModifier`, `Shake`, `ToggleMouse`, `ToggleFullscreen` (F11), and `TogglePerformanceOverlay` (F10, the [performance overlay](USAGE.md#performance-overlay)). `ToggleFullscreen` and `TogglePerformanceOverlay` accept only keyboard keys. +Supported actions are `Cross`, `Circle`, `Triangle`, `Square`, `L1`, `R1`, `L2`, `R2`, `L3`, `R3`, `Options`, `Up`, `Right`, `Down`, `Left`, `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown`, `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown`, `TouchLeft`, `TouchRight`, `StickModifier`, `Shake`, `ToggleMouse`, `ToggleFullscreen` (F11), `TogglePerformanceOverlay` (F10, the [performance overlay](USAGE.md#performance-overlay)), and `Screenshot` (F12, saves the next displayed frame as described in [screenshots](USAGE.md#screenshots)). `ToggleFullscreen`, `TogglePerformanceOverlay` and `Screenshot` accept only keyboard keys. ### Default bindings @@ -42,6 +42,7 @@ Without an `anyps5-input.ini`, or for actions the file does not mention, these b | `Shake` | X (reports a swinging acceleration and angular velocity while held) | | `ToggleMouse` | Middle mouse button, F7 | | `ToggleFullscreen` | F11 | +| `Screenshot` | F12 | For example, this changes Cross to F or Space, moves the left stick to IJKL, uses the mouse buttons for Square and R2, and keeps all other built-in bindings: diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 9b5c3d93a6..95867f7a9b 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -255,6 +255,10 @@ When a dump has neither `sce_sys/playgo-chunk.dat` nor `sce_sys/playgo-scenario. Leave the variable unset to keep the explicit error. Any other value also stops with an error. Dumps that include PlayGo metadata are unaffected. These title-specific results come from emulator runs and have not been verified against a console. +### Screenshots + +Press F12 in the game window to save the next displayed frame as a PNG at the resolution of the game's display buffer, without the window's scaling, letterbox or [performance overlay](#performance-overlay). A BT.2100 PQ display buffer is saved converted to SDR, also while it is shown through [HDR output](#hdr-output). Files are named `-<YYYYMMDD-HHMMSS>.png` and written to an `anyps5-screenshots/` directory beside the output executable, which is created when needed; set `ANYPS5_SCREENSHOTS` to use another directory. The saved path is printed to stderr. Presses while the previous screenshot is still being written are merged into one. The key is the `Screenshot` action of the [input mapping](INPUT_MAPPING.md). A screenshot that cannot be written stops the game with the error. + ### GPU selection The game runs on the first Vulkan 1.1 device with graphics and compute queues and swapchain presentation, preferring a discrete GPU over an integrated one. Set `ANYPS5_GPU` to a part of a device name, compared without regard to case, to run on another device; the names are printed at start-up in the `Physical device candidate` lines. When no usable device contains the text, the start fails and the error lists the device names. @@ -278,7 +282,7 @@ HDR output is available on Windows only. Titles see it when the monitor showing ### Shader cache -Recompiled shaders and the Vulkan pipeline cache are kept in a `shader_cache/` directory beside the output executable, so later runs skip recompilation. Set `ANYPS5_SHADER_CACHE_DIR` to use another directory, or set `ANYPS5_NO_SHADER_CACHE` to any non-empty value other than `0` to disable both caches. Shaders are stored in a subdirectory named after the recompiler's source version; subdirectories of other versions unused for 14 days are removed the first time a run looks up a shader. The pipeline cache is one `vk-pipelines-*.bin` file per GPU and driver version. Entries of the current version are never removed and the directory has no size limit, so a title that compiles many shaders can add several GB within minutes. Deleting the directory is safe: it is rebuilt on the next run. +Recompiled shaders and the Vulkan pipeline cache are kept in a `shader_cache/` directory beside the output executable, so later runs skip recompilation. Set `ANYPS5_SHADER_CACHE_DIR` to use another directory, or set `ANYPS5_NO_SHADER_CACHE` to any non-empty value other than `0` to disable both caches. Shaders are stored in a subdirectory named after the recompiler's source version; subdirectories of other versions unused for 14 days are removed the first time a run looks up a shader. The pipeline cache is one `vk-pipelines-*.bin` file per GPU and driver version in the same subdirectory, so it goes with the shaders it was built from. Entries of the current version are never removed and the directory has no size limit, so a title that compiles many shaders can add several GB within minutes. Deleting the directory is safe: it is rebuilt on the next run. ### Pipeline journal @@ -296,6 +300,10 @@ GPU results in storage images and render targets stay on the GPU until something A title's own CPU reads of that memory are not seen, so a title that waits for a GPU label and then reads the results reads stale memory. `APS5_STORE_AT_LABEL=1` stores every pending result before each label the GPU writes (`RELEASE_MEM`, `WRITE_DATA`, `DMA_DATA`), at the cost of a write-back of every pending image per label. Any other value stops the title at its first label. +### Benchmark reports + +To measure how a title runs on a machine, start it with `APS5_TRACE_SUMMARY_EVERY=30 APS5_TRACE_BENCH=1`, play the scene for a few minutes and quit. Every 30 seconds the runtime rewrites `aps5-bench-<commit>-<pid>.json.gz` in `APS5_TRACE_DIR` or the working directory and appends a line to `aps5-summary.jsonl`. The file holds the time between every two presents, the commit, GPU, driver, OS, CPU name, thread count and physical memory, and, when `app0/sce_sys/param.json` is present, the title ID, name and content version. It holds no game data. The first interval includes shader compilation; compare later intervals. The interval still open when the title exits is not written. The fields are described under [summaries for a long session](../dev/BUILD.md#summaries-for-a-long-session). + ### Tracing system library calls Windows only. 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: sequence number, thread, library, import name as the executable imports it (a NID) and the caller's address. They are followed by the 32 most called imports since start-up, with their number of calls. diff --git a/tools/build_web.py b/tools/build_web.py index cd19113603..c62f3a080e 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -1,13 +1,17 @@ import argparse import hashlib +import io import json import os from pathlib import Path import re import shutil +import tempfile import urllib.request import zipfile +from package_release import WEB_FILES + ROOT = Path(__file__).resolve().parent.parent MAX_EXPANDED_BYTES = 512 * 1024 * 1024 @@ -28,9 +32,10 @@ def request(url): return urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=60) -def publish_libraries(output): - with request("https://api.github.com/repos/boykopovar/AnyPS5/releases/latest") as response: - release = json.load(response) +def publish_libraries(output, release=None): + if release is None: + with request("https://api.github.com/repos/boykopovar/AnyPS5/releases/latest") as response: + release = json.load(response) tag = release["tag_name"] if not re.fullmatch(r"v[0-9A-Za-z][0-9A-Za-z._-]*", tag): raise ValueError("Invalid release tag") @@ -90,18 +95,69 @@ def publish_libraries(output): print(f"Published library assets from {tag}") +def publish_release(output): + with request("https://api.github.com/repos/boykopovar/AnyPS5/releases/latest") as response: + release = json.load(response) + tag = release["tag_name"] + if not re.fullmatch(r"v[0-9A-Za-z][0-9A-Za-z._-]*", tag) or tag.endswith("."): + raise ValueError("Invalid release tag") + name = f"web-{tag}.zip" + matches = [asset for asset in release["assets"] if asset["name"] == name] + if len(matches) != 1: + raise ValueError(f"Missing or ambiguous release asset: {name}; publish a release with the web converter") + asset = matches[0] + if not 0 < asset["size"] <= 128 * 1024 * 1024: + raise ValueError("Invalid web release archive size") + with request(asset["browser_download_url"]) as response: + data = response.read(asset["size"] + 1) + if len(data) != asset["size"]: + raise ValueError("Incomplete or oversized web release archive") + checksum = hashlib.sha256(data).hexdigest() + if asset.get("digest") and asset["digest"] != "sha256:" + checksum: + raise ValueError("Web release archive checksum mismatch") + output.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix=".web-release-", dir=output.parent) as temporary: + staging = Path(temporary) + with zipfile.ZipFile(io.BytesIO(data)) as archive: + files = archive.infolist() + expected = {*WEB_FILES, "web-release.json"} + if len(files) != len(expected) or {file.filename for file in files} != expected: + raise ValueError("Invalid web release archive layout") + if any(file.file_size == 0 for file in files) or sum(file.file_size for file in files) > 128 * 1024 * 1024: + raise ValueError("Invalid expanded web release archive size") + metadata = json.loads(archive.read("web-release.json")) + if (not isinstance(metadata, dict) or metadata.get("tag") != tag + or not isinstance(metadata.get("revision"), str) + or not re.fullmatch(r"[0-9a-f]{40}", metadata["revision"])): + raise ValueError("Web converter does not match the selected release") + for file in files: + target = staging / file.filename + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(archive.read(file)) + publish_libraries(staging, release) + manifest = json.loads((staging / "release.json").read_text(encoding="utf-8")) + manifest["revision"] = metadata["revision"] + (staging / "release.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + (staging / ".nojekyll").touch() + shutil.copytree(staging, output, dirs_exist_ok=True) + print(f"Published web converter and libraries from {tag} ({metadata['revision']})") + + def main(): parser = argparse.ArgumentParser() parser.add_argument("--build", type=Path, required=True) - parser.add_argument("--libraries", action="store_true") + parser.add_argument("--release", "--libraries", dest="release", action="store_true", + help="Download the matching converter and libraries from the latest release") args = parser.parse_args() output = args.build.resolve() / "web" + if args.release: + publish_release(output) + print(output) + return for name in ("relinker_wasm.mjs", "relinker_wasm.wasm"): if not (output / "native" / name).is_file(): raise ValueError(f"Build relinker_wasm first: missing {name}") copy_sources(output) - if args.libraries: - publish_libraries(output) print(output) diff --git a/tools/check_compile.py b/tools/check_compile.py index 7073b097f3..b48c0ff528 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -12,14 +12,27 @@ from pathlib import Path CPP = (".cpp", ".cc", ".cxx", ".c") DROP = ("-c", "-MD", "-MMD", "-MP") -DROP_WITH_VALUE = ("-o", "-MT", "-MF", "-MJ") +DROP_WITH_VALUE = ("-o", "-MT", "-MF", "-MQ", "-MJ") -def capture(command, cwd=None): - return subprocess.run(command, capture_output=True, text=True, encoding="utf-8", +def capture(command, cwd=None, encoding="utf-8"): + return subprocess.run(command, capture_output=True, text=True, encoding=encoding, errors="replace", cwd=cwd) +def ninja_encoding(ninja): + if sys.platform != "win32": + return "utf-8" + result = capture([ninja, "-t", "wincodepage"]) + if result.returncode != 0: + raise ValueError(f"ninja -t wincodepage: {result.stderr.strip() or result.stdout.strip()}") + declared = result.stdout.strip() + encodings = {"Build file encoding: UTF-8": "utf-8", "Build file encoding: ANSI": "mbcs"} + if declared not in encodings: + raise ValueError(f"unsupported Ninja build-file encoding: {declared!r}") + return encodings[declared] + + def repository(): result = capture(["git", "rev-parse", "--show-toplevel"]) if result.returncode != 0: @@ -36,7 +49,7 @@ def entries(build): ninja = shutil.which("ninja") if ninja is None: raise ValueError(f"no {manifest} and ninja is not on PATH") - result = capture([ninja, "-C", str(build), "-t", "compdb", "-x"]) + result = capture([ninja, "-C", str(build), "-t", "compdb", "-x"], encoding=ninja_encoding(ninja)) if result.returncode != 0: raise ValueError(f"ninja -t compdb: {result.stderr.strip()}") return json.loads(result.stdout) @@ -113,7 +126,7 @@ def syntax_arguments(arguments, directory, source): if argument in DROP_WITH_VALUE: index += 2 continue - if argument in DROP or argument.startswith("-MJ"): + if argument in DROP or argument.startswith(("-MT", "-MF", "-MQ", "-MJ")): index += 1 continue if not argument.startswith("-"): diff --git a/tools/check_conventions.py b/tools/check_conventions.py index c55fea49e7..8c2ed750d8 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -66,11 +66,30 @@ SECTION = re.compile(r"^###(?!#)\s*(.+?)\s*$", re.M) KEY = re.compile(r"^( *)([A-Za-z0-9_-]+|'[^']*'|\"[^\"]*\"):(?:\s+(.*?))?\s*$") + def git(*args, text=True): - output = subprocess.run(["git", "-c", "core.quotePath=false", *args], capture_output=True, check=True).stdout + try: + result = subprocess.run( + ["git", "-c", "core.quotePath=false", *args], + capture_output=True, + check=True, + ) + except subprocess.CalledProcessError as error: + message = error.stderr.decode("utf-8", errors="replace").strip() + print( + f"Git command failed: {message or 'unknown Git error'}", + file=sys.stderr, + ) + raise SystemExit(2) from None + except FileNotFoundError: + print("Git executable not found. Please install Git.", file=sys.stderr) + raise SystemExit(2) from None + + output = result.stdout return output.decode("utf-8", errors="replace") if text else output + class Check: def __init__(self, base, head): self.head = head diff --git a/tools/check_main_sync.py b/tools/check_main_sync.py index 12d5b78a32..e1f41677d7 100644 --- a/tools/check_main_sync.py +++ b/tools/check_main_sync.py @@ -103,11 +103,17 @@ if __name__ == "__main__": parser.error("--fetch needs a remote-tracking base such as upstream/main") git(args.repo, "fetch", "--quiet", remote, branch) - if args.fetch and args.base: - fetch(args.base) + if args.fetch: + if args.base: + fetch(args.base) + else: + remotes = set(lines(args.repo, "remote")) + for candidate in BASES: + remote, _, branch = candidate.partition("/") + if branch and remote in remotes: + fetch(candidate) + break base = resolve_base(args.repo, args.base) - if args.fetch and not args.base: - fetch(base) data = report(args.repo, base, args.head) except RuntimeError as error: parser.error(str(error)) diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index da156692b5..45faf7639a 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -53,8 +53,9 @@ def built(build): def commands(content): content = LEXICAL.sub(lambda match: re.sub(r"[^\n]", " ", match.group()) if match.group("comment") is not None else match.group(), content) + code = LEXICAL.sub(lambda match: re.sub(r"[^\n]", "_", match.group()), content) parsed_until = 0 - for command in COMMAND.finditer(content): + for command in COMMAND.finditer(code): if command.start() < parsed_until: continue position, arguments = command.end(), [] @@ -129,8 +130,11 @@ if __name__ == "__main__": parser.add_argument("build", type=Path, nargs="?", default=Path("build"), help="configured build directory") parser.add_argument("--list", action="store_true", help="print every test executable and whether ctest runs it") args = parser.parse_args() - every = sorted(built(args.build)) - run = registered(args.build) + try: + every = sorted(built(args.build)) + run = registered(args.build) + except (OSError, ValueError, subprocess.CalledProcessError) as error: + parser.error(str(error)) if args.list: for name in every: print(f"{'run' if name in run else 'NOT RUN'} {name}") diff --git a/tools/configure_seed.py b/tools/configure_seed.py index 890a19b327..465ad3b15d 100644 --- a/tools/configure_seed.py +++ b/tools/configure_seed.py @@ -10,7 +10,7 @@ ENTRY = re.compile(r"^([^#/:][^:]*):([A-Z]+)=(.*)$") def checks(cache): found, doc = {}, "" - for line in Path(cache).read_text(encoding="utf-8").splitlines(): + for line in Path(cache).read_text(encoding="utf-8-sig").splitlines(): if line.startswith("//"): doc = line continue @@ -34,11 +34,14 @@ def main(): parser.add_argument("--exclude", action="append", default=[], metavar="GLOB", help="omit matching check names after inclusion; patterns are case-sensitive") args = parser.parse_args() - found = {name: entry for name, entry in checks(args.cache).items() + cache, seed = Path(args.cache), Path(args.seed) + if cache.resolve() == seed.resolve() or (cache.exists() and seed.exists() and cache.samefile(seed)): + parser.error("seed output would overwrite the input cache") + found = {name: entry for name, entry in checks(cache).items() if (not args.include or any(fnmatch.fnmatchcase(name, pattern) for pattern in args.include)) and not any(fnmatch.fnmatchcase(name, pattern) for pattern in args.exclude)} - Path(args.seed).write_text("".join(f"set({quote(name)} {quote(value)} CACHE INTERNAL {quote(doc)})\n" - for name, (value, doc) in sorted(found.items())), encoding="utf-8") + seed.write_text("".join(f"set({quote(name)} {quote(value)} CACHE INTERNAL {quote(doc)})\n" + for name, (value, doc) in sorted(found.items())), encoding="utf-8") print(f"{len(found)} check results") return 0 diff --git a/tools/debt_split.py b/tools/debt_split.py index d6b0ef0793..728521a76a 100644 --- a/tools/debt_split.py +++ b/tools/debt_split.py @@ -71,7 +71,7 @@ def read_debt(root, name): sections = {} if path.exists(): section = None - for kind, item_section, text in parse(path.read_text(encoding="utf-8")): + for kind, item_section, text in parse(path.read_text(encoding="utf-8-sig")): if kind == "heading" and item_section and text.startswith("### "): section = item_section sections.setdefault(section, []) @@ -129,7 +129,7 @@ def index_note(): def command_split(root): - text = (root / INDEX).read_text(encoding="utf-8") + text = (root / INDEX).read_text(encoding="utf-8-sig") index, files, order = split(text) if not files: sys.exit(f"{INDEX} has no entries to move") @@ -145,7 +145,7 @@ def command_split(root): def git(root, *args, check=True): - return subprocess.run(["git", *args], cwd=root, capture_output=True, text=True, encoding="utf-8", check=check).stdout + return subprocess.run(["git", *args], cwd=root, capture_output=True, text=True, encoding="utf-8", errors="replace", check=check).stdout def apply_change(root, base, new, index_text): @@ -228,11 +228,14 @@ def command_resolve(root): print("\n".join(touched)) -def main(): +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description=f"Split {INDEX} into one file per library or component, and move entries of a change made before the split") parser.add_argument("command", choices=("split", "resolve"), help=f"split: move the entries of {INDEX} into {DEBT_DIR}/; resolve: during a rebase stopped on {INDEX}, apply the stopped commit's entry changes to {DEBT_DIR}/ and stage them") - args = parser.parse_args() + args = parser.parse_args(argv) root = Path(git(Path.cwd(), "rev-parse", "--show-toplevel").strip()) command_split(root) if args.command == "split" else command_resolve(root) diff --git a/tools/env_reference.py b/tools/env_reference.py index 55aa42af6b..5e6c23f713 100644 --- a/tools/env_reference.py +++ b/tools/env_reference.py @@ -12,9 +12,9 @@ SKIP = {"build", "build-linux", "build-relinker", "3rdparty", ".git"} INTEGER_READERS = {"kibSetting", "MaskFromEnvironment", "EnvInt", "configured", "environmentCount"} STRING_READERS = {"GetEnvironmentVariableA", "GetEnvironmentVariableW", "addWideString"} -NULL_TEST = re.compile(r"[!=]=\s*(nullptr|NULL|0)\b|!\s*(?:std::)?getenv\(") -INTEGER_TEST = re.compile(r"\b(atoi|strtol|strtoul|strtoll|strtoull|stoi|stol|stoul|from_chars|atoi64)\s*\(") -GREP = (r'(getenv|IsEnvironmentSet|kibSetting|MaskFromEnvironment|EnvInt|GetEnvironmentVariable[AW]' +NULL_TEST = re.compile(r"[!=]=\s*(nullptr|NULL|0)\b|!\s*(?:std::)?_?w?getenv\(") +INTEGER_TEST = re.compile(r"\b(atoi|strtol|strtoul|strtoll|strtoull|stoi|stol|stoul|from_chars|atoi64|_wtoi|_wtol|_wtoi64)\s*\(") +GREP = (r'(getenv|_wgetenv|wgetenv|IsEnvironmentSet|kibSetting|MaskFromEnvironment|EnvInt|GetEnvironmentVariable[AW]' r'|configured|PipelineSwitch|environmentCount|addWideString)' r'\([[:space:]]*(u|L)?"(APS5|ANYPS5)_[A-Za-z0-9_]+"') @@ -28,6 +28,7 @@ def forms(prefixes=PREFIXES): name = _name(prefixes) return ( ("getenv", re.compile(r'(?:std::)?getenv\(\s*"(?P<name>' + name + r')"\s*\)')), + ("_wgetenv", re.compile(r'(?:std::)?_?wgetenv\(\s*L"(?P<name>' + name + r')"\s*\)')), ("IsEnvironmentSet", re.compile(r'IsEnvironmentSet\(\s*"(?P<name>' + name + r')"\s*\)')), ("MaskFromEnvironment", re.compile(r'MaskFromEnvironment\(\s*"(?P<name>' + name + r')"\s*\)')), ("kibSetting", re.compile(r'kibSetting\(\s*"(?P<name>' + name + r')"\s*,\s*(?P<default>[0-9]+)')), diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index 0f1238023c..58f66cf38c 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -402,13 +402,13 @@ class TargetOverrideTests(unittest.TestCase): encoding="utf-8") binary = root / ("oracle.cmd" if os.name == "nt" else "oracle") if os.name == "nt": - binary.write_text(f'@"{sys.executable}" "{runner}" %*\n', encoding="utf-8") + binary.write_text('@"%HW_ORACLE_TEST_PYTHON%" "%~dp0probe.py" %*\n', encoding="ascii") else: binary.write_text(f'#!/bin/sh\nexec {shlex.quote(sys.executable)} {shlex.quote(str(runner))} "$@"\n', encoding="utf-8") binary.chmod(0o755) with patch.object(hw_oracle, "oracle", return_value=binary), \ - patch.dict(os.environ, {"HW_ORACLE_TARGET": ""}): + patch.dict(os.environ, {"HW_ORACLE_TARGET": "", "HW_ORACLE_TEST_PYTHON": sys.executable}): os.environ["ROCR_VISIBLE_DEVICES"] = "0" self.assertEqual(hw_oracle.detected_target(binary, "1"), "gfx1100") for visible, expected in (("0", "gfx1030"), ("0", "gfx1030"), diff --git a/tools/import_audit.py b/tools/import_audit.py index 4887d6d288..d3a5151df5 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -534,6 +534,30 @@ def check_conservation(references, records): raise AuditError(f"conservation failed: {references} references in, {classified} classified, unknown classes {unknown}") +MODULE_FILE_SUFFIXES = (".debug_prx", ".suprx", ".sprx", ".prx") + + +def module_stem(name, fold): + for suffix in MODULE_FILE_SUFFIXES: + if len(name) > len(suffix) and name.endswith(suffix): + name = name[:-len(suffix)] + break + return name.lower() if fold else name + + +class ModuleNames(set): + """Title module names, matched like the relinker: by the name without its .prx, .sprx, .suprx or .debug_prx suffix, and + ignoring case for Windows output.""" + + def __init__(self, names=(), fold=False): + super().__init__(names) + self.fold = fold + self.stems = {module_stem(name, fold) for name in self} + + def __contains__(self, library): + return isinstance(library, str) and module_stem(library, self.fold) in self.stems + + def classify(nid, library, providers, stubs, modules): if library and library in modules: return "module" @@ -557,13 +581,13 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu for record in records.values(): nid, library = record["nid"], record["library"] providers = exports.get(nid, []) - if library is None: + if not library: providers = sorted(set(providers) | set((module_exports or {}).get(nid, []))) resolved_nid = nid if not providers: resolved_nid = nid_names.compute_nid(nid) providers = exports.get(resolved_nid, []) - if library is None: + if not library: providers = sorted(set(providers) | set((module_exports or {}).get(resolved_nid, []))) kind = classify(resolved_nid, library, providers, stubs, modules) record["class"] = kind @@ -731,6 +755,7 @@ def main(argv=None): module_imports, module_exports = read_modules(args.modules) modules = set(module_imports) modules |= {name.removesuffix(".guest.prx") for name in modules if name.endswith(".guest.prx")} + modules = ModuleNames(modules, fold=bool(windows)) records, missing = audit(imports, exports, source_stubs(args.source), libraries, modules, module_imports, module_exports, needed_libraries=needed) missing_hosts = audit_host_dependencies(host_dependencies, libraries, modules, windows) if args.names: diff --git a/tools/package_release.py b/tools/package_release.py index 25474b757e..f85a46a74b 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -1,4 +1,5 @@ import argparse +import json import os import re import shutil @@ -9,10 +10,12 @@ import zipfile from pathlib import Path ROOT = Path(__file__).resolve().parent.parent +WEB_FILES = ("index.html", "app.css", "app.mjs", "archive.mjs", "batch.mjs", "output-directory.mjs", "worker.mjs", + "native/relinker_wasm.mjs", "native/relinker_wasm.wasm") def windows_runtime(build): - for line in (build / "CMakeCache.txt").read_text(encoding="utf-8").splitlines(): + for line in (build / "CMakeCache.txt").read_text(encoding="utf-8-sig").splitlines(): if line.startswith("CMAKE_CXX_COMPILER:"): compiler = line.partition("=")[2] if compiler and not compiler.endswith("-NOTFOUND"): @@ -24,7 +27,7 @@ def tracy_runtime(platform, build): cache = build / "CMakeCache.txt" if not cache.is_file(): return [] - lines = cache.read_text(encoding="utf-8").splitlines() + lines = cache.read_text(encoding="utf-8-sig").splitlines() enabled = next((line.partition("=")[2].upper() for line in lines if line.startswith("APS5_ENABLE_TRACY:")), "OFF") if enabled in ("", "0", "OFF", "NO", "FALSE", "N", "IGNORE", "NOTFOUND") or enabled.endswith("-NOTFOUND"): return [] @@ -95,22 +98,54 @@ def collect_docs(source, output): shutil.copy2(document, output / document.name) +def package_web(build, output, version, revision): + if not re.fullmatch(r"v[0-9A-Za-z][0-9A-Za-z._-]*", version) or version.endswith("."): + raise ValueError(f"Invalid release tag for asset filenames: {version}") + if not re.fullmatch(r"[0-9a-f]{40}", revision): + raise ValueError("Web release revision must be a full commit SHA") + source = build / "web" + for name in WEB_FILES: + file = source / name + if not file.is_file() or file.stat().st_size == 0: + raise RuntimeError(f"Missing or empty web release file: {file}") + name = f"web-{version}.zip" + destination = output / name + if destination.exists() and not destination.is_file(): + raise RuntimeError(f"Release destination is not a file: {destination}") + output.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix=".release-", dir=output) as temporary: + asset = Path(temporary) / name + with zipfile.ZipFile(asset, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: + for file in WEB_FILES: + archive.write(source / file, arcname=file) + archive.writestr("web-release.json", json.dumps({"tag": version, "revision": revision}) + "\n") + asset.replace(destination) + + def main(): parser = argparse.ArgumentParser() - parser.add_argument("--platform", choices=("linux", "windows")) + parser.add_argument("--platform", choices=("linux", "windows", "web")) parser.add_argument("--build", type=Path) parser.add_argument("--version") + parser.add_argument("--revision") parser.add_argument("--docs", type=Path) parser.add_argument("--output", required=True, type=Path) args = parser.parse_args() if args.docs is not None: - if args.platform is not None or args.build is not None or args.version is not None: - parser.error("--docs cannot be combined with --platform, --build or --version") + if args.platform is not None or args.build is not None or args.version is not None or args.revision is not None: + parser.error("--docs cannot be combined with --platform, --build, --version or --revision") collect_docs(args.docs, args.output) else: if args.platform is None or args.build is None or args.version is None: parser.error("--platform, --build and --version are required when --docs is not specified") - package(args.platform, args.build, args.output, args.version) + if args.platform == "web": + if args.revision is None: + parser.error("--revision is required for --platform web") + package_web(args.build, args.output, args.version, args.revision) + else: + if args.revision is not None: + parser.error("--revision requires --platform web") + package(args.platform, args.build, args.output, args.version) if __name__ == "__main__": diff --git a/tools/pr_labels.py b/tools/pr_labels.py index 0ca6f4724b..4ca5c70a9b 100644 --- a/tools/pr_labels.py +++ b/tools/pr_labels.py @@ -12,8 +12,9 @@ import performance_compare UNBLOCKS = re.compile(r'Unblocks:[ \t]*([^\r\n]*(?:\r?\n[ \t]*[-*][ \t]+(?!\[[ xX]\])[^\r\n]*)*)') TITLE_ID = re.compile(r"(?<![A-Za-z0-9])[A-Z]{4}\d{5}(?!\d)") +NONE = re.compile(r"[-*_ \t\r\n]*none\b", re.I) PERF = re.compile(r"perf(\(|!?:)") -FENCE = re.compile(r"```[^\r\n]*\r?\n(.*?)```", re.S) +FENCE = re.compile(r"(?:```|~~~)[^\r\n]*\r?\n(.*?)(?:```|~~~)", re.S) MARKER = "<!-- pr-labels-perf -->" UNBLOCKS_LABEL = "unblocks title" PERF_LABEL = "perf" @@ -39,7 +40,7 @@ def strip_comments(body): def title_ids(body): match = UNBLOCKS.search(strip_comments(body)) - return set(TITLE_ID.findall(match.group(1))) if match else set() + return set(TITLE_ID.findall(match.group(1))) if match and not NONE.match(match.group(1)) else set() def is_perf(title): diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index eefdfc5ae1..1d299dbba7 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -239,6 +239,12 @@ def write_reports(out, rows, blob): (out / f"{number}.md").write_text(report(found, blob), encoding="utf-8") +def describe_failure(error): + command = " ".join(str(part) for part in error.cmd) + message = (error.stderr or "").strip() or "no error output" + return f"pr_overlap: {command} failed with exit code {error.returncode}: {message}" + + if __name__ == "__main__": parser = argparse.ArgumentParser(description="report open pull requests that overlap each other") parser.add_argument("--base", default="main") @@ -251,20 +257,23 @@ if __name__ == "__main__": parser.error("pass exactly one of --out or --branch") if args.jobs < 1: parser.error("--jobs must be at least 1") - REPO = args.repo or upstream() - prs = open_prs(args.base) - fetch(args.base, prs) - if args.branch: - sha = git("rev-parse", args.branch).strip() - prs = own(prs, sha) - prs[0] = {"ref": sha, "author": None, "depends": set()} - scan_all(prs, args.jobs) - rows = {n: {} for n in prs} - pairs = [(0, n) for n in prs if n] if args.branch else combinations(prs, 2) - for a, b in pairs: - if found := overlaps(prs, a, b): - rows[a][b] = rows[b][a] = found - blob = f'https://github.com/{REPO}/blob/{git("rev-parse", "refs/pr/base").strip()}' + try: + REPO = args.repo or upstream() + prs = open_prs(args.base) + fetch(args.base, prs) + if args.branch: + sha = git("rev-parse", args.branch).strip() + prs = own(prs, sha) + prs[0] = {"ref": sha, "author": None, "depends": set()} + scan_all(prs, args.jobs) + rows = {n: {} for n in prs} + pairs = [(0, n) for n in prs if n] if args.branch else combinations(prs, 2) + for a, b in pairs: + if found := overlaps(prs, a, b): + rows[a][b] = rows[b][a] = found + blob = f'https://github.com/{REPO}/blob/{git("rev-parse", "refs/pr/base").strip()}' + except subprocess.CalledProcessError as error: + raise SystemExit(describe_failure(error)) if args.branch: print(report(rows[0], blob) or "No overlap with open pull requests.", end="") raise SystemExit diff --git a/tools/prepare_release_notes.py b/tools/prepare_release_notes.py index 253ac3679d..0163999059 100644 --- a/tools/prepare_release_notes.py +++ b/tools/prepare_release_notes.py @@ -34,6 +34,7 @@ def prepare_notes(source, body, full): raise IsADirectoryError(str(destination)) if os.name == "nt" and destination.exists() and destination.stat().st_file_attributes & stat.FILE_ATTRIBUTE_READONLY: raise PermissionError(f"Release note destination is read-only: {destination}") + body.parent.mkdir(parents=True, exist_ok=True) full.parent.mkdir(parents=True, exist_ok=True) full.write_bytes(notes.encode("utf-8")) body.write_bytes(release_body(notes).encode("utf-8")) diff --git a/tools/progress.py b/tools/progress.py index 72113313d8..7cda38feea 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -165,7 +165,7 @@ def scan_library(path): for source in path.rglob("*.cpp"): if "tests" in source.relative_to(path).parts: continue - text = code(source.read_text(errors="ignore")) + text = code(source.read_text(encoding="utf-8", errors="ignore")) calls = stub_calls(text) wrappers = inert_wrappers(text) for match in DEFINITION.finditer(text): @@ -186,7 +186,7 @@ def scan_library(path): if not (done or todo): cmake = path / "CMakeLists.txt" texts = [(CMAKE_SHARED, cmake.read_text(encoding="utf-8", errors="ignore"))] if cmake.is_file() else [] - texts += [(SOURCE_SHARED, source.read_text(errors="ignore")) for source in sorted(path.glob("*.cpp"))] + texts += [(SOURCE_SHARED, source.read_text(encoding="utf-8", errors="ignore")) for source in sorted(path.glob("*.cpp"))] owners = [m[1] for pattern, content in texts for m in pattern.finditer(content)] owners = [o for o in owners if o != path.name and (path.parent / o).is_dir()] if owners: @@ -473,7 +473,13 @@ def main(argv=None): if args.compare: if args.module or args.json: parser.error("--compare does not combine with --module or --json") - base, head = (json.loads(path.read_text(encoding="utf-8")) for path in args.compare) + snapshots = [] + for path in args.compare: + try: + snapshots.append(json.loads(path.read_text(encoding="utf-8"))) + except (OSError, ValueError) as error: + parser.error(f"{path}: {error}") + base, head = snapshots print(report(base, head), end="") return 0 if not args.output and not (args.module or args.json): diff --git a/tools/progress_history.py b/tools/progress_history.py index ee976f90de..ca8460652a 100644 --- a/tools/progress_history.py +++ b/tools/progress_history.py @@ -8,7 +8,7 @@ import sys import subprocess import tarfile import tempfile -from datetime import date +from datetime import date, datetime, timezone from html import escape from pathlib import Path @@ -26,9 +26,9 @@ def git(*args): def daily_commits(ref): seen = {} - for line in git("log", "--first-parent", "--format=%H %cs", ref).decode().splitlines(): - sha, day = line.split() - seen.setdefault(day, sha) + for line in git("log", "--first-parent", "--format=%H %ct", ref).decode().splitlines(): + sha, timestamp = line.split() + seen.setdefault(datetime.fromtimestamp(int(timestamp), timezone.utc).date().isoformat(), sha) return sorted(seen.items()) @@ -210,7 +210,7 @@ def page(points): f'<p>Each panel has its own scale: the solid line is what is implemented, the dashed line what is declared. Library ' 'functions are declared in the <code>prx</code> sources; the shader total is the AMD RDNA 1 + RDNA 2 instruction list ' f'(<a href="{progress.SOURCE}/tools/rdna_isa.txt">tools/rdna_isa.txt</a>). ' - 'One point per day with commits on <code>main</code>, taken at the last commit of the day. Every point is measured ' + 'One point per UTC day with commits on <code>main</code>, taken at the last commit of the day. Every point is measured ' f'with the current <a href="{progress.SOURCE}/tools/progress.py">tools/progress.py</a> rules and instruction list, ' 'so the series is comparable across days. Hover a day for the exact numbers. Raw numbers: <a href="history.json">history.json</a>. ' '<a href="progress.html">Back to the current progress</a>.</p>', diff --git a/tools/select_tests.py b/tools/select_tests.py index 50c0a56334..2d1b33ed6e 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -197,6 +197,9 @@ def select(graph, changed): def plan(build, graph, picked): _, listed = build.tests() run = [i for i, t in enumerate(listed, 1) if t["name"] in picked or t["name"] not in graph["tests"]] + if not run: + skip = {build.target(t) for t in listed} - {None} + return [], [t for t in build.leaves() if t not in skip], len(listed) _, selected = build.tests("-I", "0,0,0," + ",".join(map(str, run))) names = {t["name"] for t in selected} run = [i for i, t in enumerate(listed, 1) if t["name"] in names] diff --git a/tools/stubgen.py b/tools/stubgen.py index ae9f68955b..a701cb5472 100644 --- a/tools/stubgen.py +++ b/tools/stubgen.py @@ -221,8 +221,9 @@ def main(argv=None): ap.add_argument("--td-out", required=False, default=None, help="output file for TechnicalDebt lines, which go in docs/dev/debt/MODULE.md, default stderr when code uses stdout, otherwise stdout") ap.add_argument("--td-only", action="store_true", help="print only TechnicalDebt lines, no code") args = ap.parse_args(argv) - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8", newline="\n") + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", newline="\n") try: if not args.td_only and args.out and args.td_out and same_file(args.out, args.td_out): ap.error("--out and --td-out must name different files") diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index b7c89efccb..4d11ed8cd1 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -6,6 +6,7 @@ import sys import tempfile import unittest from pathlib import Path +from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import check_compile @@ -14,6 +15,40 @@ TOOL = Path(__file__).resolve().parents[1] / "check_compile.py" class CheckCompileTests(unittest.TestCase): + def test_dependency_options_do_not_make_syntax_checks_fail_or_rewrite_outputs(self): + compiler = shutil.which("clang") + if compiler is None: + self.skipTest("Clang is required to check unused dependency options with -Werror") + source = self.source("unit.c") + object_file = self.build / "unit.o" + dependency_file = self.build / "unit.d" + for option in ("-MF", "-MT", "-MQ"): + for joined in (False, True): + with self.subTest(option=option, joined=joined): + source.write_text("int x;\n", encoding="utf-8") + value = str(dependency_file) if option == "-MF" else "target with spaces.o" + flags = [option + value] if joined else [option, value] + if option != "-MF": + flags += ["-MF", str(dependency_file)] + arguments = [compiler, "-Werror", "-MMD", *flags, + "-c", str(source), "-o", str(object_file)] + original = subprocess.run(arguments, cwd=self.build, capture_output=True, text=True) + self.assertEqual(original.returncode, 0, original.stderr) + outputs = {path: path.read_bytes() for path in (object_file, dependency_file)} + self.manifest([{"directory": str(self.build), "file": str(source), + "arguments": arguments}]) + result = self.tool("unit.c") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("1 translation units compile", result.stdout) + for path, content in outputs.items(): + self.assertEqual(path.read_bytes(), content) + source.write_text("#error syntax_check_must_fail\n", encoding="utf-8") + result = self.tool("unit.c") + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn("syntax_check_must_fail", result.stderr) + for path, content in outputs.items(): + self.assertEqual(path.read_bytes(), content) + 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: @@ -32,6 +67,43 @@ class CheckCompileTests(unittest.TestCase): self.assertFalse((self.build / "unit.o.d").exists()) self.assertFalse((self.build / "unit.o.rsp").exists()) + def test_ninja_commands_in_a_non_ascii_build_directory_are_found(self): + compiler = shutil.which("clang") or shutil.which("gcc") + if compiler is None or shutil.which("ninja") is None: + self.skipTest("needs Ninja and a C compiler") + name = "ação" + build = self.root / name + if os.name == "nt": + try: + str(build).encode("mbcs", "strict") + except UnicodeEncodeError: + self.skipTest("the ANSI code page cannot represent the build directory") + build.mkdir() + self.source("unit.c") + command = subprocess.list2cmdline([compiler]) if os.name == "nt" else '"' + compiler + '"' + (build / "build.ninja").write_text( + "rule cc\n command = " + command + " -c $in -o $out\nbuild unit.o: cc ../unit.c\n", encoding="utf-8") + result = self.tool("--build", name, "unit.c") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("1 translation units compile", result.stdout) + + def test_windows_ninja_output_is_decoded_in_its_declared_build_file_encoding(self): + cases = (("Build file encoding: UTF-8\n", 0, "utf-8"), ("Build file encoding: ANSI\n", 0, "mbcs"), + ("Build file encoding: OEM\n", 0, None), ("", 1, None)) + for declared, status, expected in cases: + with self.subTest(declared=declared, status=status): + result = subprocess.CompletedProcess([], status, declared, "ninja: error: unknown tool" if status else "") + with patch.object(check_compile.sys, "platform", "win32"), \ + patch.object(check_compile, "capture", return_value=result): + if expected is None: + with self.assertRaises(ValueError): + check_compile.ninja_encoding("ninja") + else: + self.assertEqual(check_compile.ninja_encoding("ninja"), expected) + with patch.object(check_compile.sys, "platform", "linux"), \ + patch.object(check_compile, "capture", side_effect=AssertionError("queried Ninja")): + self.assertEqual(check_compile.ninja_encoding("ninja"), "utf-8") + def test_syntax_check_preserves_existing_clang_compilation_fragment(self): compiler = shutil.which("clang") if compiler is None: @@ -327,14 +399,17 @@ class CheckCompileTests(unittest.TestCase): bin = self.root / "bin" bin.mkdir() stub = bin / "ninja_stub.py" - stub.write_text("import json\nprint(json.dumps(%r))\n" % [entry], encoding="utf-8") + stub.write_text("import json\nimport sys\n" + "if sys.argv[1:] == ['-t', 'wincodepage']:\n print('Build file encoding: UTF-8')\n" + "else:\n print(json.dumps(%r))\n" % [entry], encoding="utf-8") if os.name == "nt": - (bin / "ninja.cmd").write_text(f'@"{sys.executable}" "{stub}"\n', encoding="utf-8") + (bin / "ninja.cmd").write_text('@"%NINJA_STUB_PYTHON%" "%~dp0ninja_stub.py" %*\n', encoding="ascii") else: ninja = bin / "ninja" ninja.write_text(f'#!/bin/sh\nexec "{sys.executable}" "{stub}"\n', encoding="utf-8") ninja.chmod(0o755) - result = self.tool("unit.cpp", env={"PATH": f"{bin}{os.pathsep}{os.environ['PATH']}"}) + result = self.tool("unit.cpp", env={"PATH": f"{bin}{os.pathsep}{os.environ['PATH']}", + "NINJA_STUB_PYTHON": sys.executable}) self.assertEqual(result.returncode, 0) self.assertIn("1 translation units compile", result.stdout) diff --git a/tools/tests/test_check_conventions_git.py b/tools/tests/test_check_conventions_git.py new file mode 100644 index 0000000000..5502d752d0 --- /dev/null +++ b/tools/tests/test_check_conventions_git.py @@ -0,0 +1,41 @@ + +import subprocess +import sys +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +CHECKER = REPO_ROOT / "tools" / "check_conventions.py" +INVALID_REVISION = "this-branch-does-not-exist" + + +class GitErrorHandlingTests(unittest.TestCase): + def check_invalid_revision(self, option): + valid = "--head" if option == "--base" else "--base" + arguments = [valid, "HEAD", option, INVALID_REVISION] + result = subprocess.run( + [ + sys.executable, + str(CHECKER), + *arguments, + ], + cwd=REPO_ROOT, + capture_output=True, + text=True, + ) + + self.assertEqual(result.returncode, 2) + self.assertIn("Git command failed:", result.stderr) + self.assertIn(INVALID_REVISION, result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_invalid_base_branch(self): + self.check_invalid_revision("--base") + + def test_invalid_head_branch(self): + self.check_invalid_revision("--head") + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_main_sync.py b/tools/tests/test_check_main_sync.py index d893e70b0e..a4590a60e5 100644 --- a/tools/tests/test_check_main_sync.py +++ b/tools/tests/test_check_main_sync.py @@ -185,6 +185,14 @@ class CheckMainSyncTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("merge: clean", result.stdout) + def test_default_base_is_fetched_before_it_is_resolved(self): + with tempfile.TemporaryDirectory() as other: + subprocess.run(["git", "clone", "-q", self.directory.name, other], check=True, capture_output=True) + self.repo.git("remote", "add", "upstream", other) + result = subprocess.run([sys.executable, str(TOOL), "--repo", self.directory.name, "--fetch"], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("merge: clean", result.stdout) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py index a627ba96ce..9c1b936b20 100644 --- a/tools/tests/test_configure_seed.py +++ b/tools/tests/test_configure_seed.py @@ -1,3 +1,4 @@ +import os import shutil import subprocess import sys @@ -24,17 +25,88 @@ set(MINI_OPTION "value" CACHE STRING "An option, not a check result") """ -@unittest.skipUnless(shutil.which("ninja") and (shutil.which("gcc") or shutil.which("cc")), "needs ninja and a C compiler") -class ConfigureSeedTests(unittest.TestCase): - def test_check_names_with_spaces_round_trip_through_cmake(self): - cache = self.root / "named-check.txt" - cache.write_text('//Test HAVE FEATURE\nHAVE FEATURE:INTERNAL=1\n', encoding="utf-8") - output = self.configure("named-check", "-C", str(self.seed(cache, "named-check.cmake"))) - self.assertNotRegex(output, r"CMake Error") - seeded = self.root / "named-check" / "CMakeCache.txt" - self.assertEqual(configure_seed.checks(seeded)["HAVE FEATURE"][0], "1") - self.assertNotIn("HAVE:INTERNAL=", seeded.read_text(encoding="utf-8")) +class ConfigureSeedPathTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.root = Path(self.directory.name) + self.cache = self.root / "CMakeCache.txt" + self.original = b"//Path to a program.\nCMAKE_AR:FILEPATH=/usr/bin/ar\n//Test HAVE_FEATURE\nHAVE_FEATURE:INTERNAL=1\n" + self.cache.write_bytes(self.original) + def run_seed(self, seed, *options): + return subprocess.run([sys.executable, str(TOOL), str(self.cache), str(seed), *options], + capture_output=True, text=True) + + def assert_rejected(self, seed, *options): + result = self.run_seed(seed, *options) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertEqual(result.stdout, "") + self.assertIn("seed output would overwrite the input cache", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(self.cache.read_bytes(), self.original) + self.assertEqual(Path(seed).read_bytes(), self.original) + + def test_same_path_preserves_cache(self): + self.assert_rejected(self.cache) + + def test_normalized_path_preserves_cache(self): + (self.root / "nested").mkdir() + self.assert_rejected(self.root / "nested" / ".." / self.cache.name) + + def test_hard_link_preserves_cache(self): + seed = self.root / "seed.cmake" + os.link(self.cache, seed) + self.assert_rejected(seed) + + def test_symbolic_link_preserves_cache(self): + seed = self.root / "seed.cmake" + try: + seed.symlink_to(self.cache) + except OSError as error: + self.skipTest(f"symbolic links unavailable: {error}") + self.assert_rejected(seed) + + def test_distinct_output_preserves_cache(self): + seed = self.root / "seed.cmake" + for existing in (False, True): + with self.subTest(existing=existing): + if existing: + seed.write_text("old seed", encoding="utf-8") + result = self.run_seed(seed) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, "1 check results\n") + self.assertEqual(seed.read_text(encoding="utf-8"), + 'set("HAVE_FEATURE" "1" CACHE INTERNAL "Test HAVE_FEATURE")\n') + self.assertEqual(self.cache.read_bytes(), self.original) + + def test_filters_do_not_allow_overwriting_cache(self): + for options in (("--include", "HAVE_*"), ("--include", "NO_MATCH_*"), ("--exclude", "HAVE_*")): + with self.subTest(options=options): + self.assert_rejected(self.cache, *options) + + def test_filters_preserve_cache_with_distinct_output(self): + self.original += b"//Test OTHER_CHECK\nOTHER_CHECK:INTERNAL=0\n" + self.cache.write_bytes(self.original) + seed = self.root / "seed.cmake" + feature = 'set("HAVE_FEATURE" "1" CACHE INTERNAL "Test HAVE_FEATURE")\n' + other = 'set("OTHER_CHECK" "0" CACHE INTERNAL "Test OTHER_CHECK")\n' + cases = ( + (("--include", "HAVE_*", "--include", "OTHER_*"), feature + other, 2), + (("--include", "*", "--exclude", "OTHER_*"), feature, 1), + (("--include", "have_*"), "", 0), + ) + for options, expected, count in cases: + with self.subTest(options=options): + result = self.run_seed(seed, *options) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, f"{count} check results\n") + self.assertEqual(seed.read_text(encoding="utf-8"), expected) + self.assertEqual(self.cache.read_bytes(), self.original) + + +@unittest.skipUnless(shutil.which("ninja") and (shutil.which("gcc") or shutil.which("cc")), "needs ninja and a C compiler") +class ConfigureSeedProject(unittest.TestCase): @classmethod def setUpClass(cls): cls.directory = tempfile.TemporaryDirectory() @@ -58,31 +130,14 @@ class ConfigureSeedTests(unittest.TestCase): subprocess.run([sys.executable, str(TOOL), str(cache), str(seed)], check=True, capture_output=True) return seed - def test_seeded_configure_runs_no_check_and_gets_the_same_results(self): - self.assertRegex(self.fresh, r"Looking for|Performing Test") - output = self.configure("seeded", "-C", str(self.seed(self.root / "fresh" / "CMakeCache.txt", "seed.cmake"))) - self.assertNotRegex(output, r"Looking for|Performing Test") - fresh = configure_seed.checks(self.root / "fresh" / "CMakeCache.txt") - self.assertEqual(configure_seed.checks(self.root / "seeded" / "CMakeCache.txt"), fresh) - self.assertEqual(fresh["MINI_HAVE_STDIO_H"][0], "1") - self.assertEqual(fresh["MINI_FAILS"][0], "") +class ConfigureSeedTests(ConfigureSeedProject): def test_only_check_results_are_seeded(self): found = configure_seed.checks(self.root / "fresh" / "CMakeCache.txt") self.assertEqual({n for n in found if n.startswith("MINI_")}, {"MINI_HAVE_STDIO_H", "MINI_HAVE_MISSING_H", "MINI_HAVE_PRINTF", "MINI_COMPILES", "MINI_FAILS"}) self.assertNotIn("CMAKE_C_COMPILER", found) - def test_values_with_cmake_syntax_round_trip(self): - cache = self.root / "odd" / "CMakeCache.txt" - cache.parent.mkdir() - value = r'a;b "c" \d ${e} $f' - cache.write_text(f"//Test MINI_ODD\nMINI_ODD:INTERNAL={value}\n", encoding="utf-8") - self.configure("odd-seeded", "-C", str(self.seed(cache, "odd.cmake"))) - seeded = (self.root / "odd-seeded" / "CMakeCache.txt").read_text(encoding="utf-8") - self.assertIn(f"MINI_ODD:INTERNAL={value}\n", seeded) - self.assertEqual(configure_seed.checks(self.root / "odd-seeded" / "CMakeCache.txt")["MINI_ODD"][0], value) - def test_cache_without_checks_writes_an_empty_seed(self): cache = self.root / "empty" / "CMakeCache.txt" cache.parent.mkdir() @@ -112,6 +167,45 @@ class ConfigureSeedTests(unittest.TestCase): self.assertEqual(seed.read_text(encoding="utf-8"), "") +class ConfigureSeedRoundTripTests(ConfigureSeedProject): + def test_check_names_with_spaces_round_trip_through_cmake(self): + cache = self.root / "named-check.txt" + cache.write_text('//Test HAVE FEATURE\nHAVE FEATURE:INTERNAL=1\n', encoding="utf-8") + output = self.configure("named-check", "-C", str(self.seed(cache, "named-check.cmake"))) + self.assertNotRegex(output, r"CMake Error") + seeded = self.root / "named-check" / "CMakeCache.txt" + self.assertEqual(configure_seed.checks(seeded)["HAVE FEATURE"][0], "1") + self.assertNotIn("HAVE:INTERNAL=", seeded.read_text(encoding="utf-8")) + + def test_seeded_configure_runs_no_check_and_gets_the_same_results(self): + self.assertRegex(self.fresh, r"Looking for|Performing Test") + output = self.configure("seeded", "-C", str(self.seed(self.root / "fresh" / "CMakeCache.txt", "seed.cmake"))) + self.assertNotRegex(output, r"Looking for|Performing Test") + fresh = configure_seed.checks(self.root / "fresh" / "CMakeCache.txt") + self.assertEqual(configure_seed.checks(self.root / "seeded" / "CMakeCache.txt"), fresh) + self.assertEqual(fresh["MINI_HAVE_STDIO_H"][0], "1") + self.assertEqual(fresh["MINI_FAILS"][0], "") + + def test_values_with_cmake_syntax_round_trip(self): + cache = self.root / "odd" / "CMakeCache.txt" + cache.parent.mkdir() + value = r'a;b "c" \d ${e} $f' + cache.write_text(f"//Test MINI_ODD\nMINI_ODD:INTERNAL={value}\n", encoding="utf-8") + self.configure("odd-seeded", "-C", str(self.seed(cache, "odd.cmake"))) + seeded = (self.root / "odd-seeded" / "CMakeCache.txt").read_text(encoding="utf-8") + self.assertIn(f"MINI_ODD:INTERNAL={value}\n", seeded) + self.assertEqual(configure_seed.checks(self.root / "odd-seeded" / "CMakeCache.txt")["MINI_ODD"][0], value) + + + +class ChecksParsingTests(unittest.TestCase): + def test_checks_reads_cache_with_utf8_bom(self): + with tempfile.TemporaryDirectory() as directory: + cache = Path(directory) / "CMakeCache.txt" + cache.write_bytes(b"\xef\xbb\xbf//Test FIRST_CHECK\nFIRST_CHECK:INTERNAL=1\n") + found = configure_seed.checks(cache) + self.assertEqual(found, {"FIRST_CHECK": ("1", "Test FIRST_CHECK")}) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_debt_split.py b/tools/tests/test_debt_split.py index 3596e47a76..d2d25e96be 100644 --- a/tools/tests/test_debt_split.py +++ b/tools/tests/test_debt_split.py @@ -5,6 +5,9 @@ import tempfile import unittest from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import debt_split + TOOL = Path(__file__).resolve().parents[1] / "debt_split.py" INDEX = "docs/dev/TechnicalDebt.md" @@ -222,6 +225,15 @@ class DebtSplitTests(unittest.TestCase): self.assertIn(paragraph, self.read(INDEX)) self.assertNotIn(paragraph, self.read("docs/dev/debt/libSceA.md")) + def test_read_debt_and_split_accept_utf8_bom(self): + self.write("docs/dev/debt/libSceA.md", "\ufeff### Silent stubs\n\n- entry 1\n") + sections = debt_split.read_debt(self.root, "libSceA") + self.assertEqual(sections, {"Silent stubs": ["- entry 1"]}) + self.write(INDEX, "\ufeff" + ORIGINAL) + self.split() + self.assertTrue((self.root / "docs/dev/debt/libSceA.md").is_file()) + self.assertIn("libSceA technical debt", self.read("docs/dev/debt/libSceA.md")) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py index ae390c5e9d..a101a5fe2d 100644 --- a/tools/tests/test_env_reference.py +++ b/tools/tests/test_env_reference.py @@ -25,6 +25,10 @@ static std::size_t m = environmentCount("APS5_PM4_RING", 16, MaximumCapacity, "A static int n = GetEnvironmentVariableW(L"ANYPS5_LOADEXEC_ARGV_SKIP", skip, 2); static const auto* o = addWideString(u"ANYPS5_LOADEXEC_PARENT"); static unsigned long p = reconfigured("APS5_NOT_A_READ", 1); +static bool q = _wgetenv(L"APS5_WIDE_PRESENT") != nullptr; +static int r = _wtoi(_wgetenv(L"APS5_WIDE_INT")); +static const wchar_t* s = _wgetenv(L"ANYPS5_WIDE_CONFIG"); +static const char* t = std::getenv("NOT_AN_ENV"); """ @@ -46,7 +50,8 @@ class EnvReferenceTests(unittest.TestCase): "APS5_WORKER_AFFINITY_MASK", "ANYPS5_GPU", "APS5_THREAD_NICE", "APS5_CRASH_INDIRECT_MEMORY", "APS5_PIPELINE_JOURNAL_MAX_MIB", "APS5_PIPELINE_STORE_ENTRIES", "APS5_PIPELINE_JOURNAL_THREADS", "APS5_PM4_RING", - "ANYPS5_LOADEXEC_ARGV_SKIP", "ANYPS5_LOADEXEC_PARENT"}) + "ANYPS5_LOADEXEC_ARGV_SKIP", "ANYPS5_LOADEXEC_PARENT", + "ANYPS5_WIDE_CONFIG", "APS5_WIDE_PRESENT", "APS5_WIDE_INT"}) self.assertEqual(found["APS5_PROFILE_DRAW"][0][1], 1) self.assertEqual(found["APS5_THREAD_NICE"][0][2:], ("integer", "-10")) self.assertEqual(found["APS5_HOST_IMPORT_MIB"][0][2:], ("integer", "6144")) @@ -56,6 +61,9 @@ class EnvReferenceTests(unittest.TestCase): self.assertEqual(found["APS5_PM4_RING"][0][2:], ("integer", "16")) self.assertEqual(found["ANYPS5_LOADEXEC_ARGV_SKIP"][0][2:], ("string", "")) self.assertEqual(found["ANYPS5_LOADEXEC_PARENT"][0][2:], ("string", "")) + self.assertEqual(found["ANYPS5_WIDE_CONFIG"][0][2:], ("string", "")) + self.assertEqual(found["APS5_WIDE_PRESENT"][0][2:], ("presence", "")) + self.assertEqual(found["APS5_WIDE_INT"][0][2:], ("integer", "")) def test_the_default_prefixes_cover_both(self): root = self.fixture() diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 98db7f5c7c..3f63361e23 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -585,6 +585,26 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual(records[("AAAAAAAAAAA", "provider.data")]["providers"], ["libSceA.prx"]) self.assertEqual(records[("BBBBBBBBBBB", "consumer.prx")]["providers"], ["provider.data"]) + def test_module_matches_imports_that_name_another_module_suffix(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting()) + (work.modules / "shipping.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, [("AAAAAAAAAAA", "shipping.debug_prx")]) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (0, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["missing_libraries"], {}) + record = next(record for record in data["imports"] if record["library"] == "shipping.debug_prx") + self.assertEqual(record["class"], "module") + + def test_module_names_ignore_case_for_windows_output_only(self): + imports = [("AAAAAAAAAAA", "SDK-Shipping.debug_prx", False)] + records, missing = import_audit.audit(imports, {}, {}, set(), import_audit.ModuleNames({"sdk-shipping.prx"}, fold=True)) + self.assertEqual((records[0]["class"], missing), ("module", {})) + records, missing = import_audit.audit(imports, {}, {}, set(), import_audit.ModuleNames({"sdk-shipping.prx"})) + self.assertEqual((records[0]["class"], missing), ("absent", {"SDK-Shipping.debug_prx": 1})) + def test_converted_module_matches_imports_of_its_base_name(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting()) @@ -890,6 +910,50 @@ class ImportAuditTests(unittest.TestCase): record, = json.loads(result.read_text())["imports"] self.assertEqual((record["nid"], record["class"], record["providers"]), (nid, "absent", [])) + def test_unscoped_registry_imports_use_guest_module_exports(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting(nid_names.compute_nid("strlen"))) + provider = work.modules / "provider.prx.guest.prx" + registry, result = work.root / "game.registry.json", work.root / "audit.json" + names = [MESSAGE_NID + "#export#module", "strlen"] + symbols = [(name, 1, GLOBAL_FUNCTION) for name in names] + entries = [(MESSAGE_NID + "#import#module", ""), (MESSAGE_NID, ""), + (MESSAGE_NAME, ""), ("strlen", "")] + for layout, image in (("sections", build_elf(symbols)), + ("sysv", build_sectionless_elf(symbols)), + ("gnu", build_sectionless_elf(symbols, gnu_hash=True)), + ("pe", build_pe(sorted(names)))): + for unresolved in (False, True): + with self.subTest(layout=layout, unresolved=unresolved): + provider.write_bytes(image) + write_registry(registry, entries + ([("missing_function", "")] if unresolved else [])) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (int(unresolved), "")) + data = json.loads(result.read_text()) + self.assertEqual(data["references"], 4 + int(unresolved)) + self.assertEqual(data["unique_by_class"], + {"implemented": 3, "stub": 0, "absent": int(unresolved), "module": 0}) + records = {record["nid"]: record for record in data["imports"]} + for symbol in (MESSAGE_NID, MESSAGE_NAME, "strlen"): + self.assertEqual(records[symbol]["providers"], [provider.name]) + self.assertFalse(records[symbol]["library_mismatch"]) + self.assertEqual(records[MESSAGE_NID]["references"], 2) + self.assertEqual(data["missing_libraries"], {}) + self.assertEqual(data["stops"][0]["nid"], "missing_function" if unresolved else None) + + def test_named_registry_import_does_not_use_unrelated_guest_export(self): + work = self.work + (work.libs / "libSceExpected.prx").write_bytes(exporting()) + (work.modules / "other.prx").write_bytes(exporting(MESSAGE_NID)) + registry, result = work.root / "game.registry.json", work.root / "audit.json" + write_registry(registry, [(MESSAGE_NID, "libSceExpected.prx")]) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + record, = data["imports"] + self.assertEqual((record["class"], record["providers"]), ("absent", [])) + self.assertEqual(data["stops"][0]["nid"], MESSAGE_NID) + def test_clean_audit_exits_zero_and_empty_registry_is_empty(self): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index b3487ae20c..0ee65bb86b 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -82,6 +82,17 @@ class ReleasePackageTests(unittest.TestCase): self.assertEqual(list(self.output.iterdir()), [previous]) self.assertEqual(previous.read_bytes(), b"existing output") + def test_windows_runtime_decodes_cmake_cache_with_utf8_bom(self): + compiler = self.root / "custom toolchain" / "bin" / "g++.exe" + (self.build / "CMakeCache.txt").write_bytes(b"\xef\xbb\xbfCMAKE_CXX_COMPILER:STRING=" + compiler.as_posix().encode("utf-8") + b"\n") + self.assertEqual(package_release.windows_runtime(self.build), compiler.parent) + + def test_tracy_runtime_decodes_cmake_cache_with_utf8_bom(self): + client = self.build / "core/libs/libs/libTracyClient.dll" + client.write_bytes(b"client") + (self.build / "CMakeCache.txt").write_bytes(b"\xef\xbb\xbfAPS5_ENABLE_TRACY:BOOL=ON\n") + self.assertEqual(package_release.tracy_runtime("windows", self.build), [client]) + def test_tracy_packages_include_only_the_target_platform_client(self): directory = self.build / "core/libs/libs" for platform, name in (("linux", "libTracyClient.so.0.14.1"), ("windows", "libTracyClient.dll")): @@ -556,9 +567,10 @@ class ReleaseNotesEdgeCaseTests(unittest.TestCase): directory = Path(directory) source_path = directory / "source.json" source_path.write_text(source, encoding="utf-8") - body_path = directory / "nested" / "body.md" - full_path = directory / "nested" / "full.md" + body_path = directory / "nested_body" / "body.md" + full_path = directory / "nested_full" / "full.md" prepare_release_notes.prepare_notes(source_path, body_path, full_path) + self.assertEqual(body_path.read_text(encoding="utf-8"), notes) self.assertEqual(full_path.read_text(encoding="utf-8"), notes) def test_body_over_limit_without_whats_changed_is_returned_unchanged(self): diff --git a/tools/tests/test_pr_labels.py b/tools/tests/test_pr_labels.py index 73a0ccd559..eec489a51a 100644 --- a/tools/tests/test_pr_labels.py +++ b/tools/tests/test_pr_labels.py @@ -38,6 +38,9 @@ class TitleIdTests(unittest.TestCase): ("- [x] Unblocks: PPSA01325 #3232\n", {"PPSA01325"}), ("- [ ] Unblocks: **PPSA01325 #3232, CUSA12345 #1**", {"PPSA01325", "CUSA12345"}), ("- [ ] Unblocks: none\nSeen on PPSA01325.\n", set()), + ("- [x] Unblocks: none by itself; foundation for PPSA27836 in #6570\n", set()), + ("- [x] Unblocks: None (PPSA03912 #5146 is already in game; this is a speedup)\n", set()), + ("- [x] Unblocks: PPSA01325 #3232 (none of the others)\n", {"PPSA01325"}), ("- [ ] Unblocks:\n - PPSA01325 #3232\n - PPSA00001\n- [ ] Depends on: none\n", {"PPSA01325", "PPSA00001"}), ("- [ ] Unblocks:\n- [ ] Depends on: PPSA01325\n", set()), ("- [ ] Unblocks: <!-- PPSA01325 -->\n", set()), @@ -72,6 +75,7 @@ class NumbersTests(unittest.TestCase): (summary("a") + "\n{not json", False), ("{}\n{}", False), ("```json\n" + json.dumps(json.loads(summary("a")), indent=2) + "\n```\n```json\n" + summary("b") + "\n```", True), + ("~~~json\n" + json.dumps(json.loads(summary("a")), indent=2) + "\n~~~\n~~~json\n" + summary("b") + "\n~~~", True), ("<!-- " + summary("a") + " -->\n" + summary("b"), False), (summary("a") + "\r\n" + summary("b") + "\r\n", True), (summary("a") + "\n{" + "[" * 100000 + "\n" + summary("b"), True), diff --git a/tools/tests/test_pr_overlap_errors.py b/tools/tests/test_pr_overlap_errors.py new file mode 100644 index 0000000000..c5bb5e0b8e --- /dev/null +++ b/tools/tests/test_pr_overlap_errors.py @@ -0,0 +1,41 @@ +import runpy +import subprocess +import sys +import unittest +from pathlib import Path +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import pr_overlap + +SCRIPT = Path(pr_overlap.__file__) +MISSING_REF = "fatal: couldn't find remote ref refs/heads/upstream/main" + + +def fake_run(command, **kwargs): + if command[0] == "gh": + return subprocess.CompletedProcess(command, 0, stdout="[]", stderr="") + return subprocess.CompletedProcess(command, 128, stdout=b"", stderr=MISSING_REF.encode()) + + +class ErrorReportTests(unittest.TestCase): + def test_failure_names_the_command_the_exit_code_and_the_git_message(self): + error = subprocess.CalledProcessError(128, ["git", "fetch", "-q"], output="", stderr=MISSING_REF + "\n") + message = pr_overlap.describe_failure(error) + self.assertEqual(message, "pr_overlap: git fetch -q failed with exit code 128: " + MISSING_REF) + + def test_failure_without_output_says_so(self): + error = subprocess.CalledProcessError(1, ["gh", "api"], output="", stderr="") + self.assertEqual(pr_overlap.describe_failure(error), "pr_overlap: gh api failed with exit code 1: no error output") + + def test_unknown_base_branch_exits_with_the_git_message_instead_of_a_traceback(self): + argv = ["pr_overlap.py", "--repo", "owner/project", "--base", "upstream/main", "--branch", "HEAD"] + with patch.object(subprocess, "run", side_effect=fake_run), patch.object(sys, "argv", argv): + with self.assertRaises(SystemExit) as raised: + runpy.run_path(str(SCRIPT), run_name="__main__") + self.assertIn("exit code 128", str(raised.exception.code)) + self.assertIn(MISSING_REF, str(raised.exception.code)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_progress.py b/tools/tests/test_progress.py index 84c3479696..9755ae2139 100644 --- a/tools/tests/test_progress.py +++ b/tools/tests/test_progress.py @@ -387,5 +387,19 @@ class ProgressTests(unittest.TestCase): self.assertIn("--compare does not combine with --module or --json", stderr.getvalue()) + def test_compare_names_a_snapshot_it_cannot_read(self): + with tempfile.TemporaryDirectory() as directory: + good = Path(directory) / "good.json" + good.write_text("{}", encoding="utf-8") + broken = Path(directory) / "broken snapshot.json" + broken.write_text("not json", encoding="utf-8") + for name, path in (("invalid", broken), ("missing", Path(directory) / "missing snapshot.json")): + with self.subTest(name): + stderr = io.StringIO() + with patch("sys.stderr", stderr), self.assertRaises(SystemExit) as raised: + progress.main(["--compare", str(good), str(path)]) + self.assertEqual(raised.exception.code, 2) + self.assertIn(str(path), stderr.getvalue()) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_progress_history.py b/tools/tests/test_progress_history.py index 4dc9ff288a..813b596990 100644 --- a/tools/tests/test_progress_history.py +++ b/tools/tests/test_progress_history.py @@ -14,13 +14,13 @@ import progress_history OPCODES = Path("core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp") -def commit(root, day, message, body): +def commit(root, day, message, body, time="12:00:00Z"): library = root / "core/libs/prx/libSceExample" library.mkdir(parents=True, exist_ok=True) (library / "Export.cpp").write_text(body) (root / OPCODES).parent.mkdir(parents=True, exist_ok=True) (root / OPCODES).write_text("enum class RdnaOpcode : int {\n SEndpgm,\n};\n") - env = {**os.environ, "GIT_AUTHOR_DATE": f"{day}T12:00:00Z", "GIT_COMMITTER_DATE": f"{day}T12:00:00Z"} + env = {**os.environ, "GIT_AUTHOR_DATE": f"{day}T{time}", "GIT_COMMITTER_DATE": f"{day}T{time}"} subprocess.run(["git", "-C", str(root), "add", "-A"], check=True) subprocess.run(["git", "-C", str(root), "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "-m", message], check=True, env=env) @@ -98,6 +98,17 @@ class ProgressHistoryTests(unittest.TestCase): self.assertIn("<svg", html) self.assertIn("2026-01-02", html) + def test_days_are_utc_whatever_the_committer_offset(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + subprocess.run(["git", "-C", directory, "init", "-q"], check=True) + commit(root, "2026-01-02", "older", "int APS5_VABI A() { return 0; }\n", "08:00:00+13:00") + commit(root, "2026-01-01", "newer", "int APS5_VABI A() { return 0; }\nint APS5_VABI B() { return 0; }\n", + "21:30:00+02:00") + with patch.multiple(progress, ROOT=root, PRX=root / "core/libs/prx", OPCODES=root / OPCODES): + points = progress_history.collect("HEAD") + self.assertEqual([(p["date"], p["libraries"]["total"]) for p in points], [("2026-01-01", 2)]) + def test_long_series_keeps_the_chart_readable(self): def series(count): start = date(2026, 1, 1).toordinal() diff --git a/tools/tests/test_progress_read_encoding.py b/tools/tests/test_progress_read_encoding.py index 1c01153b58..40f3d7e02a 100644 --- a/tools/tests/test_progress_read_encoding.py +++ b/tools/tests/test_progress_read_encoding.py @@ -36,6 +36,59 @@ class ProgressReadEncodingTests(unittest.TestCase): self.assertEqual(result.returncode, 0, stderr) self.assertEqual(result.stdout.decode("ascii").strip(), "libSceOther") + def test_utf8_cpp_source_with_ascii_default_codec(self): + source_code = """ +import sys +from pathlib import Path +sys.path.insert(0, sys.argv[1]) +import progress +group = progress.scan_library(Path(sys.argv[2]) / "libSceExample") +print(group.get("done_names")) +""" + with tempfile.TemporaryDirectory(prefix="progress read cpp ") as directory: + root = Path(directory) + library = root / "libSceExample" + library.mkdir() + (library / "Export.cpp").write_bytes( + ("// \u65e5\u672c\u8a9e \U0001f3ae\n" + "int APS5_VABI sceExampleReady() { return 0; }\n").encode("utf-8") + ) + env = {key: value for key, value in os.environ.items() if key != "PYTHONIOENCODING"} + env.update({"PYTHONUTF8": "0", "LC_ALL": "C", "LANG": "C"}) + result = subprocess.run([sys.executable, "-I", "-X", "utf8=0", "-c", source_code, str(TOOLS), str(root)], + env=env, capture_output=True, timeout=20) + stderr = result.stderr.decode("utf-8", errors="replace") + self.assertNotIn("UnicodeDecodeError", stderr) + self.assertEqual(result.returncode, 0, stderr) + self.assertEqual(result.stdout.decode("ascii").strip(), "['sceExampleReady']") + + def test_utf8_shared_cpp_with_ascii_default_codec(self): + source_code = """ +import sys +from pathlib import Path +sys.path.insert(0, sys.argv[1]) +import progress +group = progress.scan_library(Path(sys.argv[2]) / "libSceExample") +print(group.get("shared_sources")) +""" + with tempfile.TemporaryDirectory(prefix="progress shared cpp ") as directory: + root = Path(directory) + (root / "libSceOther").mkdir() + library = root / "libSceExample" + library.mkdir() + (library / "Forward.cpp").write_bytes( + ("// \u2010 \u2192 \u65e5\u672c\u8a9e\n" + '#include "prx/libSceOther/Shared.cpp"\n').encode("utf-8") + ) + env = {key: value for key, value in os.environ.items() if key != "PYTHONIOENCODING"} + env.update({"PYTHONUTF8": "0", "LC_ALL": "C", "LANG": "C"}) + result = subprocess.run([sys.executable, "-I", "-X", "utf8=0", "-c", source_code, str(TOOLS), str(root)], + env=env, capture_output=True, timeout=20) + stderr = result.stderr.decode("utf-8", errors="replace") + self.assertNotIn("UnicodeDecodeError", stderr) + self.assertEqual(result.returncode, 0, stderr) + self.assertEqual(result.stdout.decode("ascii").strip(), "libSceOther") + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index d739a0c62a..3ff32bc4ee 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -204,5 +204,19 @@ class CaseInsensitivePathTests(unittest.TestCase): self.assertIsNone(build.repo(os.path.join(os.sep, "WORK", "SOURCE", "BUILD", "Generated.h"))) +class PlanTests(unittest.TestCase): + def test_plan_empty_run_avoids_invoking_ctest(self): + build = mock.Mock() + build.tests.return_value = ({}, [{"name": "alpha", "command": ["/build/alpha_tests"]}]) + build.leaves.return_value = ["tests/alpha_tests"] + build.target.side_effect = lambda t: "tests/alpha_tests" + graph = {"tests": {"alpha": {}}} + run, leaves, count = select_tests.plan(build, graph, set()) + self.assertEqual(run, []) + self.assertEqual(leaves, []) + self.assertEqual(count, 1) + build.tests.assert_called_once_with() + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_shader_cache_runtime_sources.py b/tools/tests/test_shader_cache_runtime_sources.py index db2f418cfb..0c462bb7ea 100644 --- a/tools/tests/test_shader_cache_runtime_sources.py +++ b/tools/tests/test_shader_cache_runtime_sources.py @@ -7,6 +7,9 @@ import sys import tempfile import unittest +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import select_tests + SOURCE = Path(sys.argv.pop(1)) CMAKE = sys.argv.pop(1) @@ -74,7 +77,7 @@ file(WRITE "${{CMAKE_CURRENT_BINARY_DIR}}/runtime-sources.txt" "${{runtimeSource raise AssertionError('The generator fixture invoked a compiler: ' + (self.root / 'compiler-invoked').read_text(encoding='utf-8')) self.generated = self.build / 'shader_recompiler_probe_generated' self.listing = self.generated / 'sources.txt' - ninja = (self.build / 'build.ninja').read_text(encoding='utf-8') + ninja = (self.build / 'build.ninja').read_text(encoding=select_tests.Build(self.build).encoding) command = next(line for line in ninja.splitlines() if line.startswith(' COMMAND = ') and 'ShaderCacheVersion.cmake' in line) self.extra = re.search(r'"-DEXTRA=([^"]*)"', command).group(1) self.compiled = [] diff --git a/tools/tests/test_stubgen_stdout_newlines.py b/tools/tests/test_stubgen_stdout_newlines.py index 110719d87b..427d45a14c 100644 --- a/tools/tests/test_stubgen_stdout_newlines.py +++ b/tools/tests/test_stubgen_stdout_newlines.py @@ -28,6 +28,11 @@ class StubgenStdoutNewlineTests(unittest.TestCase): self.assertIn(b"\n", result.stdout) self.assertNotIn(b"\r\n", result.stdout) + def test_td_on_stderr_uses_lf(self): + result = self.run_stubgen() + self.assertIn(b"\n", result.stderr) + self.assertNotIn(b"\r\n", result.stderr) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index b53754fcb7..32bbbcfbc1 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -1,6 +1,7 @@ from pathlib import Path import os import shutil +import subprocess import sys import tempfile import unittest @@ -12,6 +13,43 @@ import check_test_registration class RegistrationTests(unittest.TestCase): + def test_literal_prefixes_cannot_expose_commands_inside_arguments(self): + prefixes = (r"\#", r"\(", '"prefix" ', '"" ', '"line\nprefix" ', "[[prefix]] ", + "[=[prefix]=] ", "[===[line\nprefix]===] ") + for prefix in prefixes: + with self.subTest(prefix=prefix): + content = (f"set(example\n{prefix}add_test(ghost /build/tests/missing_tests)\n)\n" + 'add_test("real" "/build/tests/real_tests")\n') + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_commands_inside_literals_do_not_register_tests(self): + for opening, closing in (('"', '"'), ("[[", "]]"), ("[=[", "]=]"), ("[===[", "]===]")): + with self.subTest(opening=opening): + content = (f"set(example {opening}\nadd_test(ghost /build/tests/missing_tests)\n{closing})\n" + 'add_test("real" "/build/tests/real_tests")\n') + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_properties_inside_literals_do_not_disable_a_real_test(self): + for opening, closing in (('"', '"'), ("[[", "]]"), ("[=[", "]=]"), ("[===[", "]===]")): + with self.subTest(opening=opening): + content = ('add_test("real" "/build/tests/real_tests")\n' + f"set(example {opening}\nset_tests_properties(real PROPERTIES DISABLED TRUE)\n{closing})\n") + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_subdirectories_inside_literals_are_not_followed(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + ignored = build / "ignored" + ignored.mkdir() + (ignored / "CTestTestfile.cmake").write_text( + 'add_test("ghost" "/build/tests/missing_tests")\n', encoding="utf-8") + for opening, closing in (('"', '"'), ("[[", "]]"), ("[=[", "]=]"), ("[===[", "]===]")): + with self.subTest(opening=opening): + content = (f"set(example {opening}\nsubdirs(ignored)\n{closing})\n" + 'add_test("real" "/build/tests/real_tests")\n') + (build / "CTestTestfile.cmake").write_text(content, encoding="utf-8") + self.assertEqual(check_test_registration.registered(build), {"real_tests"}) + def test_disabled_property_is_local_to_each_configuration(self): for configurations in (("OFF", "TRUE"), ("TRUE", "OFF"), ("TRUE", "TRUE")): with self.subTest(configurations=configurations): @@ -236,5 +274,25 @@ class RegistrationTests(unittest.TestCase): check_test_registration.built(build) run.assert_called_once() + def run_tool(self, build): + return subprocess.run([sys.executable, "-I", str(Path(check_test_registration.__file__)), str(build)], + capture_output=True, text=True, timeout=30) + + def test_missing_build_directory_is_reported_without_a_traceback(self): + with tempfile.TemporaryDirectory() as directory: + result = self.run_tool(Path(directory) / "no build") + self.assertEqual(result.returncode, 2) + self.assertIn("error:", result.stderr) + self.assertIn("CMakeCache.txt", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_unsupported_generator_is_reported_without_a_traceback(self): + with tempfile.TemporaryDirectory() as directory: + (Path(directory) / "CMakeCache.txt").write_text("CMAKE_GENERATOR:INTERNAL=Unix Makefiles\n", encoding="utf-8") + result = self.run_tool(directory) + self.assertEqual(result.returncode, 2) + self.assertIn("requires a Ninja generator", result.stderr) + self.assertNotIn("Traceback", result.stderr) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index 8b0ca800fd..e888dc2074 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -128,6 +128,41 @@ class TraceDigestTests(unittest.TestCase): return trace_digest.digest(trace_digest.load_trace(self.trace), trace_digest.load_summary(None, self.trace), trace_digest.load_stacks(self.stacks), worst=2, top=3) + def json_output(self, trace, *arguments): + result = subprocess.run([sys.executable, trace_digest.__file__, str(trace), "--json", *arguments], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual(result.returncode, 0, result.stderr) + return json.loads(result.stdout) + + def test_utf8_bom_plain_and_compressed_traces_preserve_the_digest(self): + events = [dict(event, args={"name": "worker \u03a9"}) if event["name"] == "thread_name" and event["tid"] == 1 + else event for event in EVENTS] + text = json.dumps({"traceEvents": events}, ensure_ascii=False) + self.trace.write_bytes(gzip.compress(text.encode("utf-8"))) + expected = self.json_output(self.trace) + for trace in (self.trace, self.trace.with_suffix("")): + for encoding in ("utf-8", "utf-8-sig"): + with self.subTest(trace=trace.name, encoding=encoding): + data = text.encode(encoding) + trace.write_bytes(gzip.compress(data) if trace.suffix == ".gz" else data) + self.assertEqual(self.json_output(trace), expected) + + def test_utf8_bom_summary_preserves_the_digest(self): + expected = self.json_output(self.trace) + summary = self.trace.with_name("aps5-summary.jsonl") + summary.write_text(summary.read_text(encoding="utf-8"), encoding="utf-8-sig") + self.assertEqual(self.json_output(self.trace), expected) + + def test_utf8_bom_stacks_preserve_comments_and_frame_names(self): + for text in (STACKS, "root;module\u03a9+0x1 2\n"): + with self.subTest(comments=text.startswith("#")): + self.stacks.write_text(text, encoding="utf-8") + expected = self.json_output(self.trace, "--stacks", str(self.stacks)) + expected_stacks = trace_digest.load_stacks(self.stacks) + self.stacks.write_text(text, encoding="utf-8-sig") + self.assertEqual(self.json_output(self.trace, "--stacks", str(self.stacks)), expected) + self.assertEqual(trace_digest.load_stacks(self.stacks), expected_stacks) + def test_summary_is_the_line_of_this_trace(self): self.assertEqual(self.result()["summary"]["trace"], "aps5-trace-abc-7.json.gz") @@ -541,6 +576,14 @@ class IntervalDigestTests(unittest.TestCase): def test_window_lines_are_skipped(self): self.assertEqual(len(trace_digest.load_intervals(self.path)), 4) + def test_utf8_bom_intervals_preserve_the_command_line_digest(self): + expected = trace_digest.digest_intervals(trace_digest.load_intervals(self.path)) + self.path.write_text(self.path.read_text(encoding="utf-8"), encoding="utf-8-sig") + result = subprocess.run([sys.executable, trace_digest.__file__, str(self.path), "--json"], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(result.stdout), expected) + def test_rows(self): result = trace_digest.digest_intervals(trace_digest.load_intervals(self.path)) first, second, third, _ = result["intervals"] @@ -628,5 +671,28 @@ class IntervalDigestTests(unittest.TestCase): cells = [cell.strip() for cell in rows[2].split("|")[1:-1]] self.assertEqual(cells, ["1", "0-30", "1800", "16.6", "18.0", "20.0", "25.0", "5.0", "50 %", escaped + " 7.5"]) + def test_bom_encoded_stacks_load(self): + with tempfile.TemporaryDirectory() as directory: + stacks_file = Path(directory) / "stacks.txt" + stacks_file.write_bytes(b"\xef\xbb\xbf# samples=1 depth=1 truncated=0\nmodule.prx+0x10 1\n") + loaded = trace_digest.load_stacks(stacks_file) + self.assertEqual(loaded, [(["module.prx+0x10"], 1)]) + + def test_bom_encoded_intervals_load(self): + with tempfile.TemporaryDirectory() as directory: + intervals_file = Path(directory) / "intervals.jsonl" + intervals_file.write_bytes(b"\xef\xbb\xbf" + json.dumps({"interval_start_s": 0.0, "interval_end_s": 10.0, "presents": 600}).encode("utf-8") + b"\n") + loaded = trace_digest.load_intervals(intervals_file) + self.assertEqual(len(loaded), 1) + self.assertEqual(loaded[0]["interval_start_s"], 0.0) + + def test_bom_encoded_summary_loads(self): + with tempfile.TemporaryDirectory() as directory: + summary_file = Path(directory) / "aps5-summary.jsonl" + summary_file.write_bytes(b"\xef\xbb\xbf" + json.dumps(SUMMARY).encode("utf-8") + b"\n") + loaded = trace_digest.load_summary(str(summary_file), SUMMARY["trace"]) + self.assertEqual(loaded["commit"], SUMMARY["commit"]) + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index b6fe9000fc..f897df3616 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -197,6 +197,31 @@ class UnwrapTest(unittest.TestCase): for header in headers: self.assertLessEqual(header[2] + header[5], len(data)) + def test_segments_copied_outside_the_load_segments_keep_their_alignment(self): + loads = [(PT_LOAD, 1, 0x1234, 0x0, 0x100, PAGE), (PT_LOAD, 4, 0x1500, 0x1000, 0x88, PAGE)] + records = [(0x2804, 0x800, 0x100), (0x102804, 0xA00, 0x88)] + payloads = {0x800: pattern(1, 0x100), 0xA00: pattern(9, 0x88)} + layouts = ( + ([(PT_DYNAMIC, 6, 0x1580, 0x1040, 0x10, 8), (SCE_COMMENT, 4, 0x15A0, 0x0, 0x20, 0x10), + (PT_NOTE, 4, 0x15C0, 0x0, 0x18, 4), (SCE_VERSION, 4, 0x15D8, 0x0, 0x10, 0x10)], + [(0x302804, 0xAA0, 0x20)], {0xAA0: pattern(11, 0x20), 0xAC0: pattern(13, 0x10)}, + [pattern(9, 0x88)[0x40:0x50], pattern(11, 0x20), b"", pattern(13, 0x10)]), + ([(PT_DYNAMIC, 6, 0x1580, 0x1044, 0x10, 8)], [(0x202804, 0xAA0, 0x10)], + {0xAA0: pattern(11, 0x10)}, [pattern(11, 0x10)]), + ) + for segments, extra_records, extra_payloads, blocks in layouts: + with self.subTest(types=[hex(segment[0]) for segment in segments]): + container = build_container(loads + segments, records + extra_records, + {**payloads, **extra_payloads}) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + data, headers = read_program_headers(output) + for kind, flags, offset, vaddr, _, size, _, align in headers: + if align > 1: + self.assertEqual(offset % align, vaddr % align, hex(kind)) + self.assertEqual([data[header[2]:header[2] + header[5]] for header in headers], + [pattern(1, 0x100), pattern(9, 0x88)] + blocks) + def test_declared_offsets_are_used_when_the_container_has_no_records(self): segments = [(PT_LOAD, 1, 0x800, 0x0, 0x40, 0x1000), (PT_LOAD, 4, 0x900, 0x1000, 0x40, 0x1000)] payloads = {0x800: pattern(3, 0x40), 0x900: pattern(5, 0x40)} diff --git a/tools/tests/test_web_release.py b/tools/tests/test_web_release.py new file mode 100644 index 0000000000..e314acd160 --- /dev/null +++ b/tools/tests/test_web_release.py @@ -0,0 +1,291 @@ +import hashlib +import io +import json +import subprocess +import sys +import tempfile +import unittest +import zipfile +from pathlib import Path +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import build_web +import package_release + +FILES = ("index.html", "app.css", "app.mjs", "archive.mjs", "batch.mjs", "output-directory.mjs", "worker.mjs", + "native/relinker_wasm.mjs", "native/relinker_wasm.wasm") +REVISION = "a" * 40 +LATEST = "https://api.github.com/repos/boykopovar/AnyPS5/releases/latest" + + +def zipped(entries): + data = io.BytesIO() + with zipfile.ZipFile(data, "w", zipfile.ZIP_DEFLATED) as archive: + for name, content in entries: + archive.writestr(name, content) + return data.getvalue() + + +class WebReleaseTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.root = Path(self.directory.name) + self.build = self.root / "build" + self.output = self.build / "web" + self.output.mkdir(parents=True) + self.contents = {name: ("release " + name).encode() for name in FILES} + self.metadata = {"tag": "v1", "revision": REVISION} + self.entries = [*self.contents.items(), ("web-release.json", json.dumps(self.metadata).encode())] + self.responses = {} + self.release = {"tag_name": "v1", "html_url": "https://github.com/boykopovar/AnyPS5/releases/tag/v1", "assets": []} + self.set_asset("web-v1.zip", zipped(self.entries)) + for platform in ("windows", "linux"): + self.set_asset(f"prx-{platform}-v1.zip", zipped([("libs/libkernel.prx", platform.encode())])) + self.requests = [] + + def set_asset(self, name, content): + url = "https://example.test/" + name + self.responses[url] = content + asset = {"name": name, "browser_download_url": url, "size": len(content), + "digest": "sha256:" + hashlib.sha256(content).hexdigest()} + self.release["assets"] = [item for item in self.release["assets"] if item["name"] != name] + [asset] + return asset + + def request(self, url): + self.requests.append(url) + if url == LATEST: + if self.requests.count(LATEST) > 1: + raise AssertionError("Release selection changed during publication") + return io.BytesIO(json.dumps(self.release).encode()) + return io.BytesIO(self.responses[url]) + + def publish(self): + with patch.object(build_web, "request", self.request): + build_web.publish_release(self.output) + + def snapshot(self): + return {path.relative_to(self.output).as_posix(): path.read_bytes() + for path in self.output.rglob("*") if path.is_file()} + + def seed_previous(self): + for name, content in {"index.html": b"old page", "native/relinker_wasm.wasm": b"newer main wasm", + "release.json": b"old manifest", "libraries/old.zip": b"old libraries"}.items(): + path = self.output / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(content) + + def assert_rejected(self, message): + self.seed_previous() + previous = self.snapshot() + with self.assertRaisesRegex(ValueError, message): + self.publish() + self.assertEqual(self.snapshot(), previous) + self.assertEqual(list(self.build.glob(".web-release-*")), []) + + def test_release_replaces_main_converter_with_matching_frontend_and_libraries(self): + self.seed_previous() + self.publish() + for name, content in self.contents.items(): + self.assertEqual((self.output / name).read_bytes(), content) + self.assertEqual(json.loads((self.output / "web-release.json").read_text()), self.metadata) + manifest = json.loads((self.output / "release.json").read_text()) + self.assertEqual((manifest["tag"], manifest["revision"], manifest["url"]), + ("v1", REVISION, self.release["html_url"])) + for platform in ("windows", "linux"): + asset = manifest["platforms"][platform] + data = (self.output / asset["path"]).read_bytes() + self.assertEqual(len(data), asset["size"]) + self.assertEqual(hashlib.sha256(data).hexdigest(), asset["sha256"]) + with zipfile.ZipFile(io.BytesIO(data)) as archive: + self.assertEqual(archive.read("libs/libkernel.prx"), platform.encode()) + self.assertTrue((self.output / ".nojekyll").is_file()) + self.assertEqual(self.requests.count(LATEST), 1) + + def test_libraries_flag_also_replaces_an_incompatible_local_converter(self): + self.seed_previous() + source = self.root / "source/demo/web" + source.mkdir(parents=True) + (source / "index.html").write_bytes(b"main frontend") + (self.output / "native/relinker_wasm.mjs").write_bytes(b"main converter") + with patch.object(build_web, "ROOT", self.root / "source"), \ + patch.object(build_web, "request", self.request), \ + patch.object(sys, "argv", ["build_web.py", "--build", str(self.build), "--libraries"]): + build_web.main() + self.assertEqual((self.output / "native/relinker_wasm.wasm").read_bytes(), self.contents["native/relinker_wasm.wasm"]) + self.assertEqual((self.output / "index.html").read_bytes(), self.contents["index.html"]) + + def test_release_mode_does_not_require_a_local_wasm_build(self): + with patch.object(build_web, "request", self.request), \ + patch.object(sys, "argv", ["build_web.py", "--build", str(self.build), "--release"]): + build_web.main() + self.assertEqual((self.output / "native/relinker_wasm.mjs").read_bytes(), self.contents["native/relinker_wasm.mjs"]) + + def test_local_build_still_copies_sources_without_network(self): + source = self.root / "source/demo/web" + source.mkdir(parents=True) + (source / "index.html").write_bytes(b"local frontend") + (self.output / "native").mkdir() + for name in ("relinker_wasm.mjs", "relinker_wasm.wasm"): + (self.output / "native" / name).write_bytes(b"local wasm") + with patch.object(build_web, "ROOT", self.root / "source"), \ + patch.object(build_web, "request", side_effect=AssertionError("Unexpected network request")), \ + patch.object(sys, "argv", ["build_web.py", "--build", str(self.build)]): + build_web.main() + self.assertEqual((self.output / "index.html").read_bytes(), b"local frontend") + self.assertEqual((self.output / "native/relinker_wasm.wasm").read_bytes(), b"local wasm") + + def test_old_release_without_web_asset_preserves_the_previous_site(self): + self.release["assets"] = [asset for asset in self.release["assets"] if asset["name"] != "web-v1.zip"] + self.assert_rejected("publish a release with the web converter") + + def test_ambiguous_web_asset_is_rejected(self): + self.release["assets"].append(dict(self.release["assets"][0])) + self.assert_rejected("ambiguous release asset") + + def test_wrong_tag_or_invalid_revision_is_rejected(self): + for metadata in ({"tag": "v2", "revision": REVISION}, {"tag": "v1", "revision": "main"}, + {"tag": "v1", "revision": None}, []): + with self.subTest(metadata=metadata): + self.requests.clear() + self.set_asset("web-v1.zip", zipped([*self.contents.items(), ("web-release.json", json.dumps(metadata))])) + self.assert_rejected("does not match") + + def test_unsafe_duplicate_missing_or_empty_files_are_rejected(self): + for entries in (self.entries + [("../escape", b"bad")], self.entries + [self.entries[0]], + self.entries[1:], [(name, b"" if name == "app.mjs" else data) for name, data in self.entries]): + with self.subTest(names=[name for name, data in entries]): + self.requests.clear() + self.set_asset("web-v1.zip", zipped(entries)) + self.assert_rejected("Invalid.*web release archive") + self.assertFalse((self.build / "escape").exists()) + + def test_web_size_and_checksum_failures_preserve_previous_site(self): + for failure in ("short", "long", "checksum", "limit"): + with self.subTest(failure=failure): + self.requests.clear() + asset = self.set_asset("web-v1.zip", zipped(self.entries)) + if failure == "short": + self.responses[asset["browser_download_url"]] = b"short" + elif failure == "long": + self.responses[asset["browser_download_url"]] += b"x" + elif failure == "checksum": + asset["digest"] = "sha256:" + "0" * 64 + else: + asset["size"] = 128 * 1024 * 1024 + 1 + self.assert_rejected("(archive|checksum)") + + def test_missing_or_corrupt_library_never_publishes_the_new_converter(self): + for failure in ("missing", "corrupt"): + with self.subTest(failure=failure): + self.requests.clear() + self.set_asset("prx-linux-v1.zip", zipped([("libs/libkernel.prx", b"linux")])) + if failure == "missing": + self.release["assets"] = [asset for asset in self.release["assets"] if asset["name"] != "prx-linux-v1.zip"] + else: + self.set_asset("prx-linux-v1.zip", zipped([("../escape", b"bad")])) + self.assert_rejected("(release asset|archive layout)") + + def test_library_download_failure_preserves_previous_site_and_removes_staging(self): + self.seed_previous() + previous = self.snapshot() + + def failed_request(url): + if url.endswith("prx-linux-v1.zip"): + raise OSError("connection lost") + return self.request(url) + + with patch.object(build_web, "request", failed_request), self.assertRaisesRegex(OSError, "connection lost"): + build_web.publish_release(self.output) + self.assertEqual(self.snapshot(), previous) + self.assertEqual(list(self.build.glob(".web-release-*")), []) + + def test_invalid_release_tag_is_rejected_before_downloading_assets(self): + for tag in ("../v1", "v1."): + with self.subTest(tag=tag): + self.requests.clear() + self.release["tag_name"] = tag + self.assert_rejected("Invalid release tag") + self.assertEqual(self.requests, [LATEST]) + + def test_packaged_web_archive_is_accepted_by_the_publisher(self): + local = self.root / "local/web" + for name, content in self.contents.items(): + path = local / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(content) + dist = self.root / "dist" + result = subprocess.run([sys.executable, str(Path(package_release.__file__)), "--platform", "web", + "--build", str(local.parent), "--output", str(dist), "--version", "v1", + "--revision", REVISION], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.set_asset("web-v1.zip", (dist / "web-v1.zip").read_bytes()) + self.publish() + for name, content in self.contents.items(): + self.assertEqual((self.output / name).read_bytes(), content) + + +class WebPackageTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.root = Path(self.directory.name) + self.build = self.root / "build" + self.output = self.root / "dist" + self.output.mkdir() + self.asset = self.output / "web-v1.zip" + self.asset.write_bytes(b"previous asset") + for name in FILES: + path = self.build / "web" / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(name.encode()) + + def test_missing_or_empty_web_file_preserves_previous_archive(self): + file = self.build / "web/native/relinker_wasm.wasm" + for empty in (False, True): + with self.subTest(empty=empty): + if empty: + file.write_bytes(b"") + else: + file.unlink() + with self.assertRaisesRegex(RuntimeError, "Missing or empty web release file"): + package_release.package_web(self.build, self.output, "v1", REVISION) + self.assertEqual(self.asset.read_bytes(), b"previous asset") + + def test_package_includes_every_frontend_source(self): + frontend = {file.name: file.read_bytes() for file in (package_release.ROOT / "demo/web").iterdir() if file.is_file()} + for name, content in frontend.items(): + (self.build / "web" / name).write_bytes(content) + package_release.package_web(self.build, self.output, "v1", REVISION) + with zipfile.ZipFile(self.asset) as archive: + self.assertEqual(set(archive.namelist()), {*frontend, "native/relinker_wasm.mjs", "native/relinker_wasm.wasm", "web-release.json"}) + for name, content in frontend.items(): + self.assertEqual(archive.read(name), content) + + def test_invalid_tag_or_revision_does_not_replace_existing_archive(self): + for tag, revision in (("../v1", REVISION), ("v1.", REVISION), ("v1", "HEAD")): + with self.subTest(tag=tag, revision=revision), self.assertRaises(ValueError): + package_release.package_web(self.build, self.output, tag, revision) + self.assertEqual(self.asset.read_bytes(), b"previous asset") + + def test_archive_write_failure_preserves_previous_archive(self): + with patch.object(package_release.zipfile.ZipFile, "write", side_effect=OSError("disk full")), \ + self.assertRaisesRegex(OSError, "disk full"): + package_release.package_web(self.build, self.output, "v1", REVISION) + self.assertEqual(self.asset.read_bytes(), b"previous asset") + self.assertEqual(list(self.output.glob(".release-*")), []) + + def test_cli_rejects_missing_or_misplaced_revision_without_writing_assets(self): + base = [sys.executable, str(Path(package_release.__file__)), "--build", str(self.build), + "--output", str(self.output), "--version", "v1"] + for arguments in (["--platform", "web"], ["--platform", "linux", "--revision", REVISION]): + with self.subTest(arguments=arguments): + result = subprocess.run([*base, *arguments], capture_output=True, text=True) + self.assertEqual(result.returncode, 2) + self.assertIn("--revision", result.stderr) + self.assertEqual(self.asset.read_bytes(), b"previous asset") + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/trace_digest.py b/tools/trace_digest.py index a2dae0800f..cdfcbcf8b1 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -72,7 +72,7 @@ class Trace: def load_trace(path): opener = gzip.open if str(path).endswith(".gz") else open - with opener(path, "rt", encoding="utf-8") as handle: + with opener(path, "rt", encoding="utf-8-sig") as handle: return Trace(json.load(handle)) @@ -80,7 +80,7 @@ def load_summary(path, trace_path): candidate = Path(path) if path else Path(trace_path).with_name("aps5-summary.jsonl") if not candidate.exists(): return None - lines = [json.loads(line) for line in candidate.read_text(encoding="utf-8").splitlines() if line.strip()] + lines = [json.loads(line) for line in candidate.read_text(encoding="utf-8-sig").splitlines() if line.strip()] name = Path(trace_path).name alternate = name[:-3] if name.endswith(".gz") else name + ".gz" for filename in (name, alternate): @@ -92,7 +92,7 @@ def load_summary(path, trace_path): def load_stacks(path): stacks = [] - for number, line in enumerate(Path(path).read_text(encoding="utf-8").splitlines(), 1): + for number, line in enumerate(Path(path).read_text(encoding="utf-8-sig").splitlines(), 1): if line.startswith("#") or not line.strip(): continue frames, _, count = line.rpartition(" ") @@ -264,7 +264,7 @@ def markdown(result): def load_intervals(path): - lines = [json.loads(line) for line in Path(path).read_text(encoding="utf-8").splitlines() if line.strip()] + lines = [json.loads(line) for line in Path(path).read_text(encoding="utf-8-sig").splitlines() if line.strip()] return [line for line in lines if "interval_start_s" in line] @@ -349,8 +349,9 @@ def main(arguments=None): if __name__ == "__main__": - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8", errors="replace") + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", errors="replace") try: sys.exit(main()) except (EOFError, OSError, ValueError, zlib.error) as error: diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index 6bebe62882..80e95c7a16 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -265,7 +265,8 @@ def unwrap(path, output, verbose=False): if header["type"] == PT_LOAD or header["size"] == 0: continue if region_for(header["source"], header["size"]) is None: - regions.append({"old": header["source"], "size": header["size"], "vaddr": 0, "align": 1}) + regions.append({"old": header["source"], "size": header["size"], + "vaddr": header["vaddr"], "align": header["align"]}) regions.sort(key=lambda region: region["old"]) program_offset = struct.unpack_from("<Q", elf_header, 0x20)[0]